GitLab E2E #119
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: GitLab E2E | |
| on: | |
| workflow_run: | |
| workflows: [GitLab E2E request] | |
| types: [completed] | |
| workflow_dispatch: | |
| inputs: | |
| setup_ref: | |
| description: "Enter an exact setup-vp commit SHA or release tag. If empty, the workflow uses this commit." | |
| required: false | |
| default: "" | |
| suite: | |
| description: "Select the GitLab test suite." | |
| type: choice | |
| options: [full, required] | |
| default: full | |
| vite_plus_version: | |
| description: "Vite+ version or dist-tag to install." | |
| required: false | |
| default: latest | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| jobs: | |
| gitlab-e2e: | |
| name: GitLab E2E | |
| if: >- | |
| github.event_name == 'workflow_dispatch' || | |
| (github.event_name == 'workflow_run' && | |
| github.event.workflow_run.event == 'pull_request' && | |
| github.event.workflow_run.conclusion == 'success' && | |
| github.event.workflow_run.path == '.github/workflows/e2e-request.yml') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 35 | |
| outputs: | |
| pr_number: ${{ steps.parameters.outputs.pr_number }} | |
| setup_vp_ref: ${{ steps.parameters.outputs.setup_vp_ref }} | |
| suite: ${{ steps.parameters.outputs.suite }} | |
| vite_plus_version: ${{ steps.parameters.outputs.vite_plus_version }} | |
| pipeline_url: ${{ steps.trigger.outputs.pipeline_url }} | |
| # Keep this privileged job API-only: no PR checkout, artifacts, or caches. | |
| steps: | |
| - name: Resolve test parameters | |
| id: parameters | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| REQUEST_EVENT: ${{ github.event.workflow_run.event }} | |
| REQUEST_CONCLUSION: ${{ github.event.workflow_run.conclusion }} | |
| REQUEST_PATH: ${{ github.event.workflow_run.path }} | |
| REQUEST_TITLE: ${{ github.event.workflow_run.display_title }} | |
| REQUEST_ACTOR: ${{ github.event.workflow_run.actor.login }} | |
| REQUEST_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| REQUEST_HEAD_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }} | |
| MANUAL_SETUP_REF: ${{ inputs.setup_ref }} | |
| MANUAL_SUITE: ${{ inputs.suite }} | |
| MANUAL_VITE_PLUS_VERSION: ${{ inputs.vite_plus_version }} | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| should_run=false | |
| setup_vp_ref="" | |
| suite=full | |
| vite_plus_version=latest | |
| report_pr_number="" | |
| skip_reason='GitLab E2E requires a successful run-e2e label request for a PR or a manual workflow_dispatch run.' | |
| resolve_pr_request() { | |
| local request_pattern='^PR #([1-9][0-9]*): labeled run-e2e at ([0-9a-f]{40})$' | |
| if [ "$EVENT_NAME" != "workflow_run" ] || [ "$REQUEST_EVENT" != "pull_request" ] || | |
| [ "$REQUEST_CONCLUSION" != "success" ] || | |
| [ "$REQUEST_PATH" != ".github/workflows/e2e-request.yml" ] || | |
| ! [[ "$REQUEST_TITLE" =~ $request_pattern ]]; then | |
| return | |
| fi | |
| local pr_number="${BASH_REMATCH[1]}" | |
| if [ "${BASH_REMATCH[2]}" != "$REQUEST_HEAD_SHA" ]; then | |
| echo "::error::The request title does not match the workflow run's head SHA." | |
| exit 1 | |
| fi | |
| # Check the original labeler's access, even on reruns. | |
| # GitHub maps the maintain role to write. | |
| local permission | |
| permission="$(gh api "repos/${GITHUB_REPOSITORY}/collaborators/${REQUEST_ACTOR}/permission" --jq '.permission')" | |
| case "$permission" in | |
| admin | write) ;; | |
| *) | |
| echo "::error::Adding run-e2e requires repository write access to approve a test run." | |
| exit 1 | |
| ;; | |
| esac | |
| # Compare Git blobs to reject altered request triggers or run names. | |
| local workflow_url="repos/${GITHUB_REPOSITORY}/contents/${REQUEST_PATH}" | |
| local trusted_blob request_blob | |
| trusted_blob="$(gh api "${workflow_url}?ref=${GITHUB_SHA}" --jq '.sha')" | |
| if ! request_blob="$(gh api "${workflow_url}?ref=${REQUEST_HEAD_SHA}" --jq '.sha')" || | |
| [ "$request_blob" != "$trusted_blob" ]; then | |
| echo "::error::The PR must include e2e-request.yml unchanged from main. Update the branch, then remove and re-add run-e2e." | |
| exit 1 | |
| fi | |
| # Fork runs can have an empty workflow_run.pull_requests array. | |
| # Resolve the PR from the verified title and cross-check its head. | |
| local pull_request | |
| pull_request="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}")" | |
| if ! jq -e --arg sha "$REQUEST_HEAD_SHA" --arg head_repo "$REQUEST_HEAD_REPOSITORY" --arg repo "$GITHUB_REPOSITORY" \ | |
| '.state == "open" and .head.sha == $sha and .head.repo.full_name == $head_repo and | |
| .base.repo.full_name == $repo and .base.ref == "main" and any(.labels[]; .name == "run-e2e")' \ | |
| <<< "$pull_request" > /dev/null; then | |
| skip_reason='This approval is stale or does not match the PR: check its head, base, and run-e2e label. Review the current commit, then remove and re-add run-e2e to test it.' | |
| return | |
| fi | |
| should_run=true | |
| setup_vp_ref="$REQUEST_HEAD_SHA" | |
| report_pr_number="$pr_number" | |
| } | |
| if [ "$EVENT_NAME" = "workflow_dispatch" ]; then | |
| should_run=true | |
| setup_vp_ref="${MANUAL_SETUP_REF:-$GITHUB_SHA}" | |
| suite="${MANUAL_SUITE:-full}" | |
| vite_plus_version="${MANUAL_VITE_PLUS_VERSION:-latest}" | |
| else | |
| resolve_pr_request | |
| fi | |
| { | |
| echo "should_run=$should_run" | |
| echo "setup_vp_ref=$setup_vp_ref" | |
| echo "suite=$suite" | |
| echo "vite_plus_version=$vite_plus_version" | |
| echo "pr_number=$report_pr_number" | |
| } >> "$GITHUB_OUTPUT" | |
| if [ "$should_run" = "false" ]; then | |
| echo "$skip_reason" | |
| { | |
| echo "### GitLab E2E" | |
| echo | |
| echo "$skip_reason" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Trigger GitLab pipeline | |
| if: steps.parameters.outputs.should_run == 'true' | |
| id: trigger | |
| env: | |
| GITLAB_PROJECT_ID: "85578524" | |
| GITLAB_TRIGGER_TOKEN: ${{ secrets.GITLAB_TRIGGER_TOKEN }} | |
| SETUP_VP_REF: ${{ steps.parameters.outputs.setup_vp_ref }} | |
| TEST_SUITE: ${{ steps.parameters.outputs.suite }} | |
| VITE_PLUS_VERSION: ${{ steps.parameters.outputs.vite_plus_version }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$GITLAB_TRIGGER_TOKEN" ]; then | |
| echo "::error::This repository does not have the GITLAB_TRIGGER_TOKEN secret." | |
| exit 1 | |
| fi | |
| response="$(curl --silent --show-error --fail-with-body \ | |
| --request POST \ | |
| --form-string "token=$GITLAB_TRIGGER_TOKEN" \ | |
| --form-string "ref=main" \ | |
| --form-string "inputs[setup_vp_ref]=$SETUP_VP_REF" \ | |
| --form-string "inputs[suite]=$TEST_SUITE" \ | |
| --form-string "inputs[vite_plus_version]=$VITE_PLUS_VERSION" \ | |
| "https://gitlab.com/api/v4/projects/${GITLAB_PROJECT_ID}/trigger/pipeline")" | |
| pipeline_id="$(jq -er '.id' <<< "$response")" | |
| pipeline_url="$(jq -er '.web_url' <<< "$response")" | |
| echo "Triggered GitLab pipeline: $pipeline_url" | |
| { | |
| echo "pipeline_id=$pipeline_id" | |
| echo "pipeline_url=$pipeline_url" | |
| } >> "$GITHUB_OUTPUT" | |
| { | |
| echo "### GitLab E2E" | |
| echo | |
| echo "- Pipeline: [$pipeline_id]($pipeline_url)" | |
| echo "- setup-vp ref: \`$SETUP_VP_REF\`" | |
| echo "- Suite: \`$TEST_SUITE\`" | |
| echo "- Vite+ version: \`$VITE_PLUS_VERSION\`" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Wait for GitLab pipeline | |
| if: steps.parameters.outputs.should_run == 'true' | |
| env: | |
| GITLAB_PROJECT_ID: "85578524" | |
| GITLAB_PIPELINE_ID: ${{ steps.trigger.outputs.pipeline_id }} | |
| GITLAB_PIPELINE_URL: ${{ steps.trigger.outputs.pipeline_url }} | |
| run: | | |
| set -euo pipefail | |
| api_url="https://gitlab.com/api/v4/projects/${GITLAB_PROJECT_ID}/pipelines/${GITLAB_PIPELINE_ID}" | |
| deadline=$((SECONDS + 1800)) | |
| previous_status="" | |
| final_status="timed_out" | |
| while [ "$SECONDS" -lt "$deadline" ]; do | |
| if payload="$(curl --silent --show-error --fail --retry 3 --retry-delay 2 "$api_url")"; then | |
| status="$(jq -er '.status' <<< "$payload")" | |
| if [ "$status" != "$previous_status" ]; then | |
| echo "GitLab pipeline status: $status" | |
| previous_status="$status" | |
| fi | |
| case "$status" in | |
| success) | |
| final_status=success | |
| break | |
| ;; | |
| failed | canceled | skipped | manual) | |
| final_status="$status" | |
| break | |
| ;; | |
| esac | |
| else | |
| echo "The workflow could not read the GitLab pipeline status. It will try again." | |
| fi | |
| sleep 10 | |
| done | |
| { | |
| echo "- Result: \`$final_status\`" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| if [ "$final_status" = "success" ]; then | |
| echo "GitLab pipeline passed: $GITLAB_PIPELINE_URL" | |
| exit 0 | |
| fi | |
| echo "GitLab pipeline did not pass: $GITLAB_PIPELINE_URL" | |
| jobs_url="https://gitlab.com/api/v4/projects/${GITLAB_PROJECT_ID}/pipelines/${GITLAB_PIPELINE_ID}/jobs?per_page=100" | |
| if jobs="$(curl --silent --show-error --fail --retry 3 --retry-delay 2 "$jobs_url")"; then | |
| jq -r 'sort_by(.name)[] | "\(.status)\t\(.name)\t\(.web_url)"' <<< "$jobs" | |
| fi | |
| echo "::error::GitLab pipeline finished with status $final_status." | |
| exit 1 | |
| report-result: | |
| name: Report GitLab E2E result | |
| needs: gitlab-e2e | |
| if: always() && needs.gitlab-e2e.outputs.pr_number != '' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write | |
| concurrency: | |
| group: gitlab-e2e-comment-${{ needs.gitlab-e2e.outputs.pr_number }} | |
| cancel-in-progress: false | |
| steps: | |
| - name: Update PR comment | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| PR_NUMBER: ${{ needs.gitlab-e2e.outputs.pr_number }} | |
| SETUP_VP_REF: ${{ needs.gitlab-e2e.outputs.setup_vp_ref }} | |
| TEST_SUITE: ${{ needs.gitlab-e2e.outputs.suite }} | |
| VITE_PLUS_VERSION: ${{ needs.gitlab-e2e.outputs.vite_plus_version }} | |
| PIPELINE_URL: ${{ needs.gitlab-e2e.outputs.pipeline_url }} | |
| RESULT: ${{ needs.gitlab-e2e.result }} | |
| with: | |
| script: | | |
| const { PR_NUMBER, SETUP_VP_REF, TEST_SUITE, VITE_PLUS_VERSION, PIPELINE_URL, RESULT } = process.env; | |
| const issue = { ...context.repo, issue_number: Number(PR_NUMBER) }; | |
| const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: issue.issue_number }); | |
| if (pr.head.sha !== SETUP_VP_REF) { | |
| core.info('The PR head changed; skip this result.'); | |
| return; | |
| } | |
| const marker = '<!-- setup-vp-gitlab-e2e -->'; | |
| const comments = await github.paginate(github.rest.issues.listComments, { ...issue, per_page: 100 }); | |
| let previous; | |
| for (const comment of comments) { | |
| if (!comment.body?.startsWith(marker)) continue; | |
| if (comment.user?.login === 'github-actions[bot]') { | |
| previous = comment; | |
| break; | |
| } | |
| // Reuse a result posted by a maintainer, including before automation was added. | |
| if (!comment.user) continue; | |
| let permission; | |
| try { | |
| const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| ...context.repo, username: comment.user.login, | |
| }); | |
| permission = data.permission; | |
| } catch (error) { | |
| if (error.status === 404) continue; | |
| throw error; | |
| } | |
| if (permission === 'admin' || permission === 'write') { | |
| previous = comment; | |
| break; | |
| } | |
| } | |
| const attempt = Number(process.env.GITHUB_RUN_ATTEMPT); | |
| const previousRun = previous?.body.match(/<!-- setup-vp-gitlab-e2e-run:(\d+):(\d+) -->/); | |
| if (previousRun && (Number(previousRun[1]) > context.runId || | |
| (Number(previousRun[1]) === context.runId && Number(previousRun[2]) > attempt))) { | |
| core.info('A newer run already reported its result.'); | |
| return; | |
| } | |
| let title; | |
| if (RESULT === 'success') title = '✅ GitLab E2E passed'; | |
| else if (RESULT === 'cancelled') title = '⚠️ GitLab E2E run cancelled'; | |
| else if (!PIPELINE_URL) title = '❌ GitLab E2E could not start'; | |
| else title = '❌ GitLab E2E did not complete successfully'; | |
| const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/attempts/${attempt}`; | |
| const links = [PIPELINE_URL && `[GitLab pipeline](${PIPELINE_URL})`, `[GitHub Actions run](${runUrl})`].filter(Boolean); | |
| const body = [ | |
| marker, | |
| `<!-- setup-vp-gitlab-e2e-run:${context.runId}:${attempt} -->`, | |
| '', `### ${title}`, '', | |
| `Commit: \`${SETUP_VP_REF}\` `, | |
| `Suite: \`${TEST_SUITE}\` · Vite+ version: \`${VITE_PLUS_VERSION}\``, | |
| '', links.join(' · '), | |
| ].join('\n'); | |
| if (previous) { | |
| await github.rest.issues.updateComment({ ...context.repo, comment_id: previous.id, body }); | |
| } else { | |
| await github.rest.issues.createComment({ ...issue, body }); | |
| } |