From 2c8e20abbfe90d8e1f0d26262e9d1b6ba00e91d6 Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 15 Sep 2026 14:44:15 +0800 Subject: [PATCH 1/2] ci: require run-e2e label for GitLab E2E --- .github/workflows/e2e-request.yml | 4 +- .github/workflows/gitlab-e2e.yml | 88 ++-------- CONTRIBUTING.md | 10 +- src/gitlab/workflow.test.ts | 275 +++++++++++++----------------- 4 files changed, 139 insertions(+), 238 deletions(-) diff --git a/.github/workflows/e2e-request.yml b/.github/workflows/e2e-request.yml index 608286d..d33bc5a 100644 --- a/.github/workflows/e2e-request.yml +++ b/.github/workflows/e2e-request.yml @@ -14,9 +14,7 @@ permissions: {} jobs: request: - if: >- - github.event.label.name == 'run-e2e' && - github.event.pull_request.head.repo.full_name != github.repository + if: github.event.label.name == 'run-e2e' runs-on: ubuntu-latest timeout-minutes: 1 steps: diff --git a/.github/workflows/gitlab-e2e.yml b/.github/workflows/gitlab-e2e.yml index 3dff0d7..b382594 100644 --- a/.github/workflows/gitlab-e2e.yml +++ b/.github/workflows/gitlab-e2e.yml @@ -1,30 +1,9 @@ name: GitLab E2E on: - push: - branches: [main] - tags: ["v*"] - pull_request: - branches: [main] workflow_run: workflows: [GitLab E2E request] types: [completed] - merge_group: - 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 @@ -34,11 +13,10 @@ jobs: gitlab-e2e: name: GitLab E2E if: >- - 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' && - github.event.workflow_run.head_repository.full_name != github.repository) + 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: @@ -53,8 +31,6 @@ jobs: id: parameters env: EVENT_NAME: ${{ github.event_name }} - EVENT_REF: ${{ github.ref }} - EVENT_REF_NAME: ${{ github.ref_name }} REQUEST_EVENT: ${{ github.event.workflow_run.event }} REQUEST_CONCLUSION: ${{ github.event.workflow_run.conclusion }} REQUEST_PATH: ${{ github.event.workflow_run.path }} @@ -62,30 +38,22 @@ jobs: 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 }} - PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} - PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - PR_NUMBER: ${{ github.event.pull_request.number }} - 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=true - setup_vp_ref="$GITHUB_SHA" - suite=required + should_run=false + setup_vp_ref="" + suite=full vite_plus_version=latest report_pr_number="" - skip_reason="" + skip_reason='Only a successful run-e2e label request for a PR can approve a GitLab E2E run.' - resolve_fork_request() { - should_run=false - skip_reason='Only a successful run-e2e label request for a fork PR can approve a GitLab E2E run.' + resolve_pr_request() { local request_pattern='^PR #([1-9][0-9]*): labeled run-e2e at ([0-9a-f]{40})$' - if [ "$REQUEST_EVENT" != "pull_request" ] || [ "$REQUEST_CONCLUSION" != "success" ] || + if [ "$EVENT_NAME" != "workflow_run" ] || [ "$REQUEST_EVENT" != "pull_request" ] || + [ "$REQUEST_CONCLUSION" != "success" ] || [ "$REQUEST_PATH" != ".github/workflows/e2e-request.yml" ] || - [ "$REQUEST_HEAD_REPOSITORY" = "$GITHUB_REPOSITORY" ] || ! [[ "$REQUEST_TITLE" =~ $request_pattern ]]; then return fi @@ -114,7 +82,7 @@ jobs: 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 fork must include e2e-request.yml unchanged from main. Update the branch, then remove and re-add run-e2e." + 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 @@ -132,40 +100,10 @@ jobs: should_run=true setup_vp_ref="$REQUEST_HEAD_SHA" - suite=full report_pr_number="$pr_number" } - if [ "$EVENT_NAME" = "pull_request" ]; then - if [ "$PR_HEAD_REPOSITORY" != "$GITHUB_REPOSITORY" ]; then - should_run=false - skip_reason='A maintainer with write access can review this fork PR and add the run-e2e label to test its current commit. New commits require removing and re-adding the label. The merge queue also tests the reviewed merge commit.' - else - setup_vp_ref="$PR_HEAD_SHA" - report_pr_number="$PR_NUMBER" - while IFS= read -r changed_path; do - case "$changed_path" in - gitlab/* | src/gitlab/* | src/ci/* | dist/gitlab/* | .github/workflows/gitlab-e2e.yml | vite.config.ts | package.json | pnpm-lock.yaml | pnpm-workspace.yaml) - suite=full - break - ;; - esac - done < <(gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" --jq '.[].filename') - fi - elif [ "$EVENT_NAME" = "workflow_run" ]; then - resolve_fork_request - elif [ "$EVENT_NAME" = "merge_group" ]; then - suite=full - elif [ "$EVENT_NAME" = "workflow_dispatch" ]; then - setup_vp_ref="${MANUAL_SETUP_REF:-$GITHUB_SHA}" - suite="${MANUAL_SUITE:-full}" - vite_plus_version="${MANUAL_VITE_PLUS_VERSION:-latest}" - elif [ "$EVENT_NAME" = "push" ] && [[ "$EVENT_REF" == refs/tags/* ]]; then - setup_vp_ref="$EVENT_REF_NAME" - suite=full - elif [ "$EVENT_NAME" = "push" ]; then - suite=full - fi + resolve_pr_request { echo "should_run=$should_run" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f866990..5c7cd7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,14 +52,16 @@ Use the shared primitives under [`src/ci/`](src/ci/) for portable runtime behavi ## GitLab End-to-End Tests -Use the dedicated [GitLab test project](https://gitlab.com/fengmk2/setup-vp-gitlab-test) to test the remote integration. The [GitLab E2E workflow](.github/workflows/gitlab-e2e.yml) covers same-repository pull requests, approved fork pull requests, merge queue commits, merges, and releases. The pipeline loads the template, bootstrap script, and compiled runtime from the exact setup-vp commit or release tag under test. +Use the dedicated [GitLab test project](https://gitlab.com/fengmk2/setup-vp-gitlab-test) to test the remote integration. GitLab CI has limited capacity, so the [GitLab E2E workflow](.github/workflows/gitlab-e2e.yml) runs only for pull requests approved with the `run-e2e` label. The pipeline loads the template, bootstrap script, and compiled runtime from the exact approved PR commit. -### Fork Pull Requests +### Request a Run -After reviewing the commit, a maintainer with write access can add `run-e2e` to run the full GitLab suite. Approve the Actions run if prompted. +After reviewing the commit, a maintainer with write access can add `run-e2e` to run the full GitLab suite. This applies to both same-repository and fork PRs. Approve the Actions run if prompted. For each new commit, review the changes and remove and re-add `run-e2e`. Read the PR result comment for the status and GitLab pipeline link after each run. +Pushes, merge queue commits, merges, and release tags do not start GitLab pipelines. For a release, request a run on the final release PR commit before merging. + ## Dependency Updates Renovate opens a PR when SocketDev publishes a new `sfw-free` release. See the custom managers in [`.github/renovate.json`](.github/renovate.json) for the pinned `SFW_VERSION` values in the GitHub and portable runtimes. @@ -85,7 +87,7 @@ Publish releases as Git tags, not as an npm package. Keep `package.json.version` git status --short # must be empty ``` -3. Confirm that the release commit on `main` passes the full GitLab E2E workflow. +3. Confirm that the final release PR commit passed the full GitLab E2E workflow through the `run-e2e` label before it was merged. 4. Create the new annotated version tag and push it. For example: diff --git a/src/gitlab/workflow.test.ts b/src/gitlab/workflow.test.ts index 7fd46b2..c69d8da 100644 --- a/src/gitlab/workflow.test.ts +++ b/src/gitlab/workflow.test.ts @@ -21,12 +21,6 @@ const steps = workflow.jobs["gitlab-e2e"].steps as Array<{ const parameters = steps.find((step) => step.id === "parameters")!; const headSha = "a".repeat(40); const baseSha = "b".repeat(40); -const approvedPr = { - state: "open", - head: { sha: headSha, repo: { full_name: "contributor/setup-vp" } }, - base: { ref: "main", repo: { full_name: "upstream/setup-vp" } }, - labels: [{ name: "run-e2e" }], -}; const tempDirs: string[] = []; type ParameterResult = SpawnSyncReturns & { @@ -62,7 +56,6 @@ case "$*" in */permission*) printf '%s\\n' "$MOCK_PERMISSION" ;; *"contents/.github/workflows/e2e-request.yml?ref=$GITHUB_SHA"*) printf '%s\\n' trusted-blob ;; *"contents/.github/workflows/e2e-request.yml?ref=$REQUEST_HEAD_SHA"*) printf '%s\\n' "$MOCK_REQUEST_BLOB" ;; - */files*) printf '%s\\n' "$MOCK_CHANGED_FILES" ;; "api repos/upstream/setup-vp/pulls/123") printf '%s\\n' "$MOCK_PR" ;; *) exit 90 ;; esac @@ -88,20 +81,10 @@ esac REQUEST_ACTOR: "reviewer", REQUEST_HEAD_SHA: headSha, REQUEST_HEAD_REPOSITORY: "contributor/setup-vp", - EVENT_REF: "refs/heads/main", - EVENT_REF_NAME: "main", - PR_HEAD_REPOSITORY: "contributor/setup-vp", - PR_HEAD_SHA: headSha, - PR_NUMBER: "123", - MANUAL_SETUP_REF: "", - MANUAL_SUITE: "", - MANUAL_VITE_PLUS_VERSION: "", MOCK_GH_CALLS: calls, MOCK_GH_FAILURE: "", MOCK_PERMISSION: "write", MOCK_REQUEST_BLOB: "trusted-blob", - MOCK_PR: JSON.stringify(approvedPr), - MOCK_CHANGED_FILES: "README.md", ...overrides, }, }); @@ -120,16 +103,24 @@ esac } describe("GitLab E2E workflow", () => { - it("only accepts label events for privileged fork runs and never checks out PR code", () => { - expect(workflow.on).not.toHaveProperty("pull_request_target"); + it("only accepts PR label requests and never checks out PR code", () => { expect(requestWorkflow.on).toEqual({ pull_request: { branches: ["main"], types: ["labeled"] }, }); expect(requestWorkflow.permissions).toEqual({}); - expect(workflow.on.workflow_run).toEqual({ - workflows: [requestWorkflow.name], - types: ["completed"], + expect(requestWorkflow.jobs.request.if).toBe("github.event.label.name == 'run-e2e'"); + expect(workflow.on).toEqual({ + workflow_run: { + workflows: [requestWorkflow.name], + types: ["completed"], + }, }); + expect(workflow.jobs["gitlab-e2e"].if).toBe( + "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'", + ); expect(requestWorkflow["run-name"]).toBe( "PR #${{ github.event.pull_request.number }}: ${{ github.event.action }} ${{ github.event.label.name }} at ${{ github.event.pull_request.head.sha }}", ); @@ -141,151 +132,123 @@ describe("GitLab E2E workflow", () => { expect(step.if).toBe("steps.parameters.outputs.should_run == 'true'"); } }); +}); - it.each(["write", "admin"])( - "runs the full suite at the event SHA with %s access", - (permission) => { - const result = resolveParameters({ MOCK_PERMISSION: permission }); - expect(result.status, result.stderr).toBe(0); - expect(result.outputs).toEqual({ - should_run: "true", - setup_vp_ref: headSha, - suite: "full", - vite_plus_version: "latest", - pr_number: "123", +describe.each(["upstream/setup-vp", "contributor/setup-vp"])( + "GitLab E2E approval from %s", + (headRepository) => { + const approvedPr = { + state: "open", + head: { sha: headSha, repo: { full_name: headRepository } }, + base: { ref: "main", repo: { full_name: "upstream/setup-vp" } }, + labels: [{ name: "run-e2e" }], + }; + + function resolveRequest(overrides: Record = {}): ParameterResult { + return resolveParameters({ + REQUEST_HEAD_REPOSITORY: headRepository, + MOCK_PR: JSON.stringify(approvedPr), + ...overrides, }); - expect(result.calls).toContain("collaborators/reviewer/permission"); - expect(result.calls).not.toContain("/files"); - }, - ); + } - it.each(["read", "none", ""])("rejects approval with %s permission", (permission) => { - const result = resolveParameters({ MOCK_PERMISSION: permission }); - expect(result.status).toBe(1); - expect(result.stdout).toContain("requires repository write access"); - expect(result.outputs.should_run).toBeUndefined(); - expect(result.calls).not.toContain("/pulls/"); - }); + it.each(["write", "admin"])( + "runs the full suite at the event SHA with %s access", + (permission) => { + const result = resolveRequest({ MOCK_PERMISSION: permission }); + expect(result.status, result.stderr).toBe(0); + expect(result.outputs).toEqual({ + should_run: "true", + setup_vp_ref: headSha, + suite: "full", + vite_plus_version: "latest", + pr_number: "123", + }); + expect(result.calls).toContain("collaborators/reviewer/permission"); + expect(result.calls).not.toContain("/files"); + }, + ); - it.each(["/permission", "/pulls/", `?ref=${baseSha}`, `?ref=${headSha}`])( - "fails closed when the %s API request fails", - (endpoint) => { - const result = resolveParameters({ MOCK_GH_FAILURE: endpoint }); + it.each(["read", "none", ""])("rejects approval with %s permission", (permission) => { + const result = resolveRequest({ MOCK_PERMISSION: permission }); expect(result.status).toBe(1); + expect(result.stdout).toContain("requires repository write access"); expect(result.outputs.should_run).toBeUndefined(); - }, - ); - - it.each([ - ["new commit", { head: { ...approvedPr.head, sha: "c".repeat(40) } }], - ["different fork", { head: { ...approvedPr.head, repo: { full_name: "other/setup-vp" } } }], - ["base change", { base: { ...approvedPr.base, ref: "release" } }], - [ - "different repository", - { base: { ...approvedPr.base, repo: { full_name: "other/setup-vp" } } }, - ], - ["closed PR", { state: "closed" }], - ["removed label", { labels: [{ name: "other-label" }] }], - ])("skips a stale approval after a %s", (_reason, changes) => { - const result = resolveParameters({ MOCK_PR: JSON.stringify({ ...approvedPr, ...changes }) }); - expect(result.status, result.stderr).toBe(0); - expect(result.outputs.should_run).toBe("false"); - expect(result.outputs.pr_number).toBe(""); - expect(result.summary).toContain("This approval is stale"); - }); - - it.each([ - ["unrelated label", { REQUEST_TITLE: `PR #123: labeled bug at ${headSha}` }], - ["new push", { REQUEST_TITLE: `PR #123: synchronize run-e2e at ${headSha}` }], - ["same-repository PR", { REQUEST_HEAD_REPOSITORY: "upstream/setup-vp" }], - ["different event", { REQUEST_EVENT: "push" }], - ["failed request", { REQUEST_CONCLUSION: "failure" }], - ["different workflow", { REQUEST_PATH: ".github/workflows/fake-request.yml" }], - ["malformed title", { REQUEST_TITLE: "PR #123: labeled run-e2e at $(exit 1)" }], - ])("does not approve a privileged run for a %s", (_reason, env) => { - const result = resolveParameters(env); - expect(result.status, result.stderr).toBe(0); - expect(result.outputs.should_run).toBe("false"); - expect(result.outputs.pr_number).toBe(""); - expect(result.calls).toBe(""); - }); + expect(result.calls).not.toContain("/pulls/"); + }); - it("rejects a title that claims a different commit than the request run", () => { - const result = resolveParameters({ REQUEST_TITLE: `PR #123: labeled run-e2e at ${baseSha}` }); - expect(result.status).toBe(1); - expect(result.outputs.should_run).toBeUndefined(); - expect(result.calls).toBe(""); - }); + it.each(["/permission", "/pulls/", `?ref=${baseSha}`, `?ref=${headSha}`])( + "fails closed when the %s API request fails", + (endpoint) => { + const result = resolveRequest({ MOCK_GH_FAILURE: endpoint }); + expect(result.status).toBe(1); + expect(result.outputs.should_run).toBeUndefined(); + }, + ); - it("rejects a modified request workflow even with a valid title and write access", () => { - const result = resolveParameters({ MOCK_REQUEST_BLOB: "modified-blob" }); - expect(result.status).toBe(1); - expect(result.stdout).toContain("must include e2e-request.yml unchanged from main"); - expect(result.outputs.should_run).toBeUndefined(); - expect(result.calls).not.toContain("/pulls/"); - }); + it.each([ + ["new commit", { head: { ...approvedPr.head, sha: "c".repeat(40) } }], + [ + "different head repository", + { head: { ...approvedPr.head, repo: { full_name: "other/setup-vp" } } }, + ], + ["base change", { base: { ...approvedPr.base, ref: "release" } }], + [ + "different repository", + { base: { ...approvedPr.base, repo: { full_name: "other/setup-vp" } } }, + ], + ["closed PR", { state: "closed" }], + ["removed label", { labels: [{ name: "other-label" }] }], + ["missing label", { labels: [] }], + ])("skips a stale approval after a %s", (_reason, changes) => { + const result = resolveRequest({ MOCK_PR: JSON.stringify({ ...approvedPr, ...changes }) }); + expect(result.status, result.stderr).toBe(0); + expect(result.outputs.should_run).toBe("false"); + expect(result.outputs.pr_number).toBe(""); + expect(result.summary).toContain("This approval is stale"); + }); - it("keeps ordinary fork PR runs unprivileged even when the approval label exists", () => { - const result = resolveParameters({ EVENT_NAME: "pull_request" }); - expect(result.status, result.stderr).toBe(0); - expect(result.outputs.should_run).toBe("false"); - expect(result.outputs.pr_number).toBe(""); - expect(result.calls).toBe(""); - expect(result.summary).toContain("add the run-e2e label"); - }); + it.each([ + ["unrelated label", { REQUEST_TITLE: `PR #123: labeled bug at ${headSha}` }], + ["new push", { REQUEST_TITLE: `PR #123: synchronize run-e2e at ${headSha}` }], + ["different event", { REQUEST_EVENT: "push" }], + ["failed request", { REQUEST_CONCLUSION: "failure" }], + ["different workflow", { REQUEST_PATH: ".github/workflows/fake-request.yml" }], + ["malformed title", { REQUEST_TITLE: "PR #123: labeled run-e2e at $(exit 1)" }], + ])("does not approve a privileged run for a %s", (_reason, env) => { + const result = resolveRequest(env); + expect(result.status, result.stderr).toBe(0); + expect(result.outputs.should_run).toBe("false"); + expect(result.outputs.pr_number).toBe(""); + expect(result.calls).toBe(""); + }); - it.each([ - ["README.md", "required"], - ["README.md\nsrc/ci/version.ts", "full"], - ])("keeps suite selection for same-repository PRs changing %s", (files, suite) => { - const result = resolveParameters({ - EVENT_NAME: "pull_request", - PR_HEAD_REPOSITORY: "upstream/setup-vp", - MOCK_CHANGED_FILES: files, + it("rejects a title that claims a different commit than the request run", () => { + const result = resolveRequest({ REQUEST_TITLE: `PR #123: labeled run-e2e at ${baseSha}` }); + expect(result.status).toBe(1); + expect(result.outputs.should_run).toBeUndefined(); + expect(result.calls).toBe(""); }); - expect(result.status, result.stderr).toBe(0); - expect(result.outputs).toMatchObject({ - should_run: "true", - setup_vp_ref: headSha, - suite, - pr_number: "123", + + it("rejects a modified request workflow even with a valid title and write access", () => { + const result = resolveRequest({ MOCK_REQUEST_BLOB: "modified-blob" }); + expect(result.status).toBe(1); + expect(result.stdout).toContain("must include e2e-request.yml unchanged from main"); + expect(result.outputs.should_run).toBeUndefined(); + expect(result.calls).not.toContain("/pulls/"); }); - expect(result.calls).not.toContain("/permission"); - }); - it.each([ - ["push", { EVENT_NAME: "push" }, baseSha, "full", "latest"], - ["merge queue", { EVENT_NAME: "merge_group" }, baseSha, "full", "latest"], - ["manual defaults", { EVENT_NAME: "workflow_dispatch" }, baseSha, "full", "latest"], - [ - "release tag", - { EVENT_NAME: "push", EVENT_REF: "refs/tags/v1.19.0", EVENT_REF_NAME: "v1.19.0" }, - "v1.19.0", - "full", - "latest", - ], - [ - "manual overrides", - { - EVENT_NAME: "workflow_dispatch", - MANUAL_SETUP_REF: headSha, - MANUAL_SUITE: "required", - MANUAL_VITE_PLUS_VERSION: "0.3.1", + it.each(["pull_request", "push", "merge_group", "workflow_dispatch"])( + "skips %s events even when the PR has the approval label", + (event) => { + const result = resolveRequest({ EVENT_NAME: event }); + expect(result.status, result.stderr).toBe(0); + expect(result.outputs.should_run).toBe("false"); + expect(result.outputs.setup_vp_ref).toBe(""); + expect(result.outputs.pr_number).toBe(""); + expect(result.calls).toBe(""); + expect(result.summary).toContain("Only a successful run-e2e label request for a PR"); }, - headSha, - "required", - "0.3.1", - ], - ])("preserves %s parameters", (_name, env, ref, suite, version) => { - const result = resolveParameters(env); - expect(result.status, result.stderr).toBe(0); - expect(result.outputs).toEqual({ - should_run: "true", - setup_vp_ref: ref, - suite, - vite_plus_version: version, - pr_number: "", - }); - expect(result.calls).toBe(""); - }); -}); + ); + }, +); From b0d43d996f4774f2322e61dac56be27066d9e03c Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 15 Sep 2026 15:17:34 +0800 Subject: [PATCH 2/2] ci: preserve manual GitLab E2E dispatch --- .github/workflows/gitlab-e2e.yml | 38 +++++++++++++++++---- CONTRIBUTING.md | 8 +++-- src/gitlab/workflow.test.ts | 58 ++++++++++++++++++++++++++++---- 3 files changed, 89 insertions(+), 15 deletions(-) diff --git a/.github/workflows/gitlab-e2e.yml b/.github/workflows/gitlab-e2e.yml index b382594..ce64e7f 100644 --- a/.github/workflows/gitlab-e2e.yml +++ b/.github/workflows/gitlab-e2e.yml @@ -4,6 +4,21 @@ 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 @@ -13,10 +28,11 @@ jobs: gitlab-e2e: name: GitLab E2E if: >- - 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' + 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: @@ -38,6 +54,9 @@ jobs: 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 @@ -47,7 +66,7 @@ jobs: suite=full vite_plus_version=latest report_pr_number="" - skip_reason='Only a successful run-e2e label request for a PR can approve a GitLab E2E run.' + 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})$' @@ -103,7 +122,14 @@ jobs: report_pr_number="$pr_number" } - resolve_pr_request + 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" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c7cd7b..0fb250b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,7 +52,7 @@ Use the shared primitives under [`src/ci/`](src/ci/) for portable runtime behavi ## GitLab End-to-End Tests -Use the dedicated [GitLab test project](https://gitlab.com/fengmk2/setup-vp-gitlab-test) to test the remote integration. GitLab CI has limited capacity, so the [GitLab E2E workflow](.github/workflows/gitlab-e2e.yml) runs only for pull requests approved with the `run-e2e` label. The pipeline loads the template, bootstrap script, and compiled runtime from the exact approved PR commit. +Use the dedicated [GitLab test project](https://gitlab.com/fengmk2/setup-vp-gitlab-test) to test the remote integration. GitLab CI has limited capacity, so the [GitLab E2E workflow](.github/workflows/gitlab-e2e.yml) runs only for pull requests approved with the `run-e2e` label or manual `workflow_dispatch` requests. The pipeline loads the template, bootstrap script, and compiled runtime from the exact approved PR commit or the manually selected commit or release tag. ### Request a Run @@ -60,7 +60,9 @@ After reviewing the commit, a maintainer with write access can add `run-e2e` to For each new commit, review the changes and remove and re-add `run-e2e`. Read the PR result comment for the status and GitLab pipeline link after each run. -Pushes, merge queue commits, merges, and release tags do not start GitLab pipelines. For a release, request a run on the final release PR commit before merging. +Pushes, merge queue commits, merges, and release tags do not automatically start GitLab pipelines. + +For a manual run, use `workflow_dispatch`. Set `setup_ref` to an exact commit SHA or release tag, or leave it empty to test the selected workflow commit. Select `suite` (`full` by default) and `vite_plus_version` (`latest` by default). ## Dependency Updates @@ -87,7 +89,7 @@ Publish releases as Git tags, not as an npm package. Keep `package.json.version` git status --short # must be empty ``` -3. Confirm that the final release PR commit passed the full GitLab E2E workflow through the `run-e2e` label before it was merged. +3. Confirm that the release commit on `main` passes the full GitLab E2E workflow. Use `workflow_dispatch` with the exact commit SHA and `suite: full`. 4. Create the new annotated version tag and push it. For example: diff --git a/src/gitlab/workflow.test.ts b/src/gitlab/workflow.test.ts index c69d8da..a8a4e98 100644 --- a/src/gitlab/workflow.test.ts +++ b/src/gitlab/workflow.test.ts @@ -81,6 +81,9 @@ esac REQUEST_ACTOR: "reviewer", REQUEST_HEAD_SHA: headSha, REQUEST_HEAD_REPOSITORY: "contributor/setup-vp", + MANUAL_SETUP_REF: "", + MANUAL_SUITE: "", + MANUAL_VITE_PLUS_VERSION: "", MOCK_GH_CALLS: calls, MOCK_GH_FAILURE: "", MOCK_PERMISSION: "write", @@ -103,7 +106,7 @@ esac } describe("GitLab E2E workflow", () => { - it("only accepts PR label requests and never checks out PR code", () => { + it("only accepts PR label requests or manual runs and never checks out PR code", () => { expect(requestWorkflow.on).toEqual({ pull_request: { branches: ["main"], types: ["labeled"] }, }); @@ -114,12 +117,24 @@ describe("GitLab E2E workflow", () => { workflows: [requestWorkflow.name], types: ["completed"], }, + workflow_dispatch: { + inputs: { + setup_ref: expect.objectContaining({ required: false, default: "" }), + suite: expect.objectContaining({ + type: "choice", + options: ["full", "required"], + default: "full", + }), + vite_plus_version: expect.objectContaining({ required: false, default: "latest" }), + }, + }, }); - expect(workflow.jobs["gitlab-e2e"].if).toBe( - "github.event_name == 'workflow_run' && " + + expect(workflow.jobs["gitlab-e2e"].if.replace(/\s+/g, " ")).toBe( + "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'", + "github.event.workflow_run.path == '.github/workflows/e2e-request.yml')", ); expect(requestWorkflow["run-name"]).toBe( "PR #${{ github.event.pull_request.number }}: ${{ github.event.action }} ${{ github.event.label.name }} at ${{ github.event.pull_request.head.sha }}", @@ -128,10 +143,41 @@ describe("GitLab E2E workflow", () => { expect(steps.every((step) => !step.uses && !step.run.includes("${{"))).toBe(true); expect(parameters.env?.REQUEST_HEAD_SHA).toBe("${{ github.event.workflow_run.head_sha }}"); expect(parameters.env?.REQUEST_ACTOR).toBe("${{ github.event.workflow_run.actor.login }}"); + expect(parameters.env?.MANUAL_SETUP_REF).toBe("${{ inputs.setup_ref }}"); + expect(parameters.env?.MANUAL_SUITE).toBe("${{ inputs.suite }}"); + expect(parameters.env?.MANUAL_VITE_PLUS_VERSION).toBe("${{ inputs.vite_plus_version }}"); for (const step of steps.slice(1)) { expect(step.if).toBe("steps.parameters.outputs.should_run == 'true'"); } }); + + it.each([ + ["defaults", {}, baseSha, "full", "latest"], + [ + "overrides", + { + MANUAL_SETUP_REF: headSha, + MANUAL_SUITE: "required", + MANUAL_VITE_PLUS_VERSION: "0.3.1", + }, + headSha, + "required", + "0.3.1", + ], + ["release tag", { MANUAL_SETUP_REF: "v1.20.0" }, "v1.20.0", "full", "latest"], + ])("preserves manual run %s", (_name, env, ref, suite, version) => { + const result = resolveParameters({ EVENT_NAME: "workflow_dispatch", ...env }); + expect(result.status, result.stderr).toBe(0); + expect(result.outputs).toEqual({ + should_run: "true", + setup_vp_ref: ref, + suite, + vite_plus_version: version, + pr_number: "", + }); + expect(result.calls).toBe(""); + expect(result.summary).toBe(""); + }); }); describe.each(["upstream/setup-vp", "contributor/setup-vp"])( @@ -238,7 +284,7 @@ describe.each(["upstream/setup-vp", "contributor/setup-vp"])( expect(result.calls).not.toContain("/pulls/"); }); - it.each(["pull_request", "push", "merge_group", "workflow_dispatch"])( + it.each(["pull_request", "push", "merge_group"])( "skips %s events even when the PR has the approval label", (event) => { const result = resolveRequest({ EVENT_NAME: event }); @@ -247,7 +293,7 @@ describe.each(["upstream/setup-vp", "contributor/setup-vp"])( expect(result.outputs.setup_vp_ref).toBe(""); expect(result.outputs.pr_number).toBe(""); expect(result.calls).toBe(""); - expect(result.summary).toContain("Only a successful run-e2e label request for a PR"); + expect(result.summary).toContain("GitLab E2E requires a successful run-e2e label request"); }, ); },