Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .github/scripts/wait-for-validation-workflows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Wait for required validation workflows to complete successfully for a PR head SHA.
*
* Used by Build Documentation / Publish Dev so they do not fail while Test and
* CI/CD Pipeline are still queued or in progress.
*
* @param {object} params
* @param {import('@actions/github').GitHub} params.github
* @param {import('@actions/github').context} params.context
* @param {import('@actions/core')} params.core
* @param {string[]} [params.workflows] workflow file names to wait on
* @param {number} [params.timeoutMs] max wait (default 120 minutes)
* @param {number} [params.pollMs] poll interval (default 30 seconds)
*/
module.exports = async function waitForValidationWorkflows({
github,
context,
core,
workflows = ["ci.yml", "test.yml"],
timeoutMs = 120 * 60 * 1000,
pollMs = 30 * 1000,
}) {
if (context.eventName !== "pull_request") {
core.info(`Skipping validation wait for event: ${context.eventName}`);
return;
}

const headSha = context.payload.pull_request.head.sha;
const owner = context.repo.owner;
const repo = context.repo.repo;
const deadline = Date.now() + timeoutMs;

const labels = {
"ci.yml": "CI/CD Pipeline",
"test.yml": "Test",
};

async function latestRun(workflowId) {
const { data } = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: workflowId,
head_sha: headSha,
per_page: 5,
});
return data.workflow_runs.find((run) => run.head_sha === headSha) || null;
}

for (const workflowId of workflows) {
const label = labels[workflowId] || workflowId;
while (true) {
const run = await latestRun(workflowId);
if (!run) {
if (Date.now() >= deadline) {
core.setFailed(`Timed out waiting for ${label} to start for ${headSha}`);
return;
}
core.info(`${label}: no run for ${headSha} yet; waiting...`);
} else if (run.status !== "completed") {
if (Date.now() >= deadline) {
core.setFailed(
`Timed out waiting for ${label} (status=${run.status}) for ${headSha}`,
);
return;
}
core.info(`${label}: ${run.status} (run ${run.id}); waiting...`);
} else if (run.conclusion === "success") {
core.info(`${label}: success (run ${run.id})`);
break;
} else {
core.setFailed(
`${label} concluded '${run.conclusion}' (run ${run.id}); required before continuing`,
);
return;
}
await new Promise((resolve) => setTimeout(resolve, pollMs));
}
}

core.info("All required validation workflows passed");
};
4 changes: 3 additions & 1 deletion .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,14 @@ Workflows that run on **PR to main** use the environment **`approval-required`**
- **Triggers**: PR to `main` (runs after approval), `workflow_dispatch`
- **Purpose**: Build documentation for testing and verification
- **Runs**:
- Waits for CI/CD Pipeline and Test workflow runs to succeed for the PR head SHA
- Generate coverage report (for docs embedding)
- Generate Bandit security report (for docs embedding)
- Build documentation using patched build script
- Upload documentation artifacts
- **Rationale**:
- PRs to `main` trigger the workflow but require approval; or run manually from any branch
- Validation gate polls instead of failing while Test/CI are still in progress

---

Expand Down Expand Up @@ -166,7 +168,7 @@ Workflows that run on **PR to main** use the environment **`approval-required`**
- **Triggers**: PR to `main` (runs after approval), `workflow_dispatch`
- **Purpose**: Publish to PyPI as nightly builds
- **Runs**:
- Validates CI/CD Pipeline and Test checks have passed (for PR to main)
- Waits for CI/CD Pipeline and Test workflow runs to succeed for the PR head SHA (polls; does not fail while they are still running)
- Builds package and publishes to PyPI using `uv publish`
- Requires `PYPI_API_TOKEN` secret
- **Rationale**:
Expand Down
26 changes: 9 additions & 17 deletions .github/workflows/build-documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,24 @@ jobs:
check-validation:
name: check-validation
runs-on: ubuntu-latest
# Allow long wait while Test matrix finishes (up to ~2h in the wait script).
timeout-minutes: 150
if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request'
permissions:
contents: read
actions: read
pull-requests: read
steps:
- name: Check if validation workflows passed
- uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Wait for CI/CD Pipeline and Test to succeed
uses: actions/github-script@v7
with:
script: |
// For PRs, check if ci.yml and test.yml have passed
if (context.eventName === 'pull_request') {
const { data: checks } = await github.rest.checks.listForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.payload.pull_request.head.sha,
});
const requiredChecks = ['CI/CD Pipeline', 'Test'];
const passedChecks = checks.check_runs.filter(
check => requiredChecks.includes(check.name) && check.conclusion === 'success'
);
if (passedChecks.length < requiredChecks.length) {
core.setFailed('Required validation workflows must pass first');
}
}
// For workflow_dispatch, allow manual override
const waitForValidation = require('./.github/scripts/wait-for-validation-workflows.js');
await waitForValidation({ github, context, core });

build-docs:
name: build-docs
Expand Down
24 changes: 9 additions & 15 deletions .github/workflows/publish-pypi-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,24 @@ jobs:
check-validation:
name: check-validation
runs-on: ubuntu-latest
# Allow long wait while Test matrix finishes (up to ~2h in the wait script).
timeout-minutes: 150
if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request'
permissions:
contents: read
actions: read
pull-requests: read
steps:
- name: Check if validation workflows passed (PR to main only)
- uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Wait for CI/CD Pipeline and Test to succeed
uses: actions/github-script@v7
with:
script: |
if (context.eventName === 'pull_request') {
const { data: checks } = await github.rest.checks.listForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.payload.pull_request.head.sha,
});
const requiredChecks = ['CI/CD Pipeline', 'Test'];
const passedChecks = checks.check_runs.filter(
check => requiredChecks.includes(check.name) && check.conclusion === 'success'
);
if (passedChecks.length < requiredChecks.length) {
core.setFailed('Required validation workflows must pass first');
}
}
const waitForValidation = require('./.github/scripts/wait-for-validation-workflows.js');
await waitForValidation({ github, context, core });

publish-nightly:
name: publish-dev-to-pypi
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ MagicMock
scripts
!dev/scripts/
!dev/scripts/**
!.github/scripts/
!.github/scripts/**
compatibility_tests/
lint_outputs/
locales
Expand Down
Loading