diff --git a/.github/workflows/deploy-replicated.yml b/.github/workflows/deploy-replicated.yml index 889d8a0b..0c909c2b 100644 --- a/.github/workflows/deploy-replicated.yml +++ b/.github/workflows/deploy-replicated.yml @@ -87,3 +87,10 @@ jobs: echo "| Admin console | https://admin.${INSTANCE}.staging.all-hands-testing.dev:30000 |" echo "| App | https://app.${INSTANCE}.staging.all-hands-testing.dev |" } >> "$GITHUB_STEP_SUMMARY" + + e2e: + name: E2E Replicated + needs: deploy + uses: ./.github/workflows/e2e-replicated.yml + with: + instance: ${{ inputs.instance }} diff --git a/.github/workflows/e2e-replicated.yml b/.github/workflows/e2e-replicated.yml new file mode 100644 index 00000000..25e4bc5a --- /dev/null +++ b/.github/workflows/e2e-replicated.yml @@ -0,0 +1,43 @@ +name: E2E Replicated + +on: + workflow_call: + inputs: + instance: + type: string + required: true + +permissions: {} + +jobs: + trigger-e2e: + environment: e2e-replicated + runs-on: ubuntu-24.04 + timeout-minutes: 2 + steps: + - name: Trigger Replicated E2E + shell: bash + env: + ARGO_TOKEN: ${{ secrets.ARGO_WORKFLOWS_E2E_TOKEN }} + INSTANCE: ${{ inputs.instance }} + run: | + # Argo answers an event that matches no binding with 200 {}, the same + # as a dispatched one, so a caller typo would otherwise pass silently. + case "$INSTANCE" in + unstable|beta|stable) ;; + *) echo "::error::unknown instance \"$INSTANCE\"" ; exit 1 ;; + esac + + payload=$(jq -n \ + --arg instance "$INSTANCE" \ + '{ + instance: $instance + }') + # Deliberately not retried: a second attempt whose first already landed + # starts a duplicate suite. + curl --fail-with-body --silent --show-error --max-time 20 \ + --request POST \ + --header "Authorization: Bearer ${ARGO_TOKEN}" \ + --header "Content-Type: application/json" \ + --data "$payload" \ + https://workflows.dev.all-hands.dev/api/v1/events/openhands-e2e/replicated-deploy diff --git a/.github/workflows/test-scripts.yml b/.github/workflows/test-scripts.yml index e80c2267..f6803662 100644 --- a/.github/workflows/test-scripts.yml +++ b/.github/workflows/test-scripts.yml @@ -9,6 +9,8 @@ on: # not only when the test itself is edited. - 'charts/**' - 'replicated/**' + - '.github/workflows/deploy-replicated.yml' + - '.github/workflows/e2e-replicated.yml' - '.github/workflows/publish-release-charts.yml' - '.github/workflows/test-scripts.yml' diff --git a/scripts/test_deploy_replicated_e2e_trigger.py b/scripts/test_deploy_replicated_e2e_trigger.py new file mode 100644 index 00000000..aad857f6 --- /dev/null +++ b/scripts/test_deploy_replicated_e2e_trigger.py @@ -0,0 +1,89 @@ +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +DEPLOY_WORKFLOW = ROOT / ".github/workflows/deploy-replicated.yml" +E2E_WORKFLOW = ROOT / ".github/workflows/e2e-replicated.yml" +TEST_WORKFLOW = ROOT / ".github/workflows/test-scripts.yml" + + +def load_workflow(path: Path): + return yaml.safe_load(path.read_text(encoding="utf-8")) + + +def trigger_step(): + job = load_workflow(E2E_WORKFLOW)["jobs"]["trigger-e2e"] + return next( + step for step in job["steps"] if step.get("name") == "Trigger Replicated E2E" + ) + + +def test_e2e_workflow_can_only_be_called_by_another_workflow(): + workflow = load_workflow(E2E_WORKFLOW) + triggers = workflow[True] + + assert set(triggers) == {"workflow_call"} + assert triggers["workflow_call"] == { + "inputs": { + "instance": {"required": True, "type": "string"}, + } + } + + +def test_e2e_workflow_uses_its_environment_token_and_argo_owned_target(): + workflow = load_workflow(E2E_WORKFLOW) + job = workflow["jobs"]["trigger-e2e"] + assert job["environment"] == "e2e-replicated" + + trigger = next( + step for step in job["steps"] if step.get("name") == "Trigger Replicated E2E" + ) + assert trigger["env"] == { + "ARGO_TOKEN": "${{ secrets.ARGO_WORKFLOWS_E2E_TOKEN }}", + "INSTANCE": "${{ inputs.instance }}", + } + + command = trigger["run"] + assert "jq -n" in command + assert "instance: $instance" in command + assert "run_url" not in command + assert "target-url" not in command + assert "test_revision" not in command + assert "all-hands-testing.dev" not in command + assert "curl --fail-with-body" in command + assert ( + "https://workflows.dev.all-hands.dev/api/v1/events/" + "openhands-e2e/replicated-deploy" in command + ) + assert 'Authorization: Bearer ${ARGO_TOKEN}' in command + + +def test_e2e_workflow_rejects_an_instance_no_binding_serves(): + """A caller typo would otherwise dispatch, match nothing, and pass.""" + command = trigger_step()["run"] + assert "unstable|beta|stable" in command + + +def test_e2e_workflow_never_retries_the_dispatch(): + """A retry whose first attempt already landed starts a second suite.""" + assert "--retry" not in trigger_step()["run"] + + +def test_deploy_replicated_calls_e2e_only_after_a_successful_deploy(): + workflow = load_workflow(DEPLOY_WORKFLOW) + e2e = workflow["jobs"]["e2e"] + + assert e2e == { + "name": "E2E Replicated", + "needs": "deploy", + "uses": "./.github/workflows/e2e-replicated.yml", + "with": {"instance": "${{ inputs.instance }}"}, + } + + +def test_workflow_contract_runs_when_either_workflow_changes(): + text = TEST_WORKFLOW.read_text(encoding="utf-8") + assert "- '.github/workflows/deploy-replicated.yml'" in text + assert "- '.github/workflows/e2e-replicated.yml'" in text