Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4d99b9f
ci: complete nightly license matrix and verify security handoff
jasonqinzhou Sep 17, 2026
45f6c0c
fix(ci): close license evidence streams under strict warnings
jasonqinzhou Sep 17, 2026
65ac860
fix nightly gate skip
pvijayakrish Sep 17, 2026
df184d9
ci: manual nightly dispatch pinned to a validated commit
pvijayakrish Sep 17, 2026
07a6b70
fix(ci): retain verified nightly handoffs while repairing skip gates
jasonqinzhou Sep 17, 2026
a2d4aec
Merge PR 250 artifact handoff fixes into nightly dispatch
jasonqinzhou Sep 17, 2026
558c25a
fix(ci): validate manual nightly sources without public version colli…
jasonqinzhou Sep 17, 2026
7211c12
docs(ci): keep dispatch guidance compatible with license follow-up
jasonqinzhou Sep 17, 2026
fb2e93b
docs(ci): separate manual dispatch instructions from release handoff
jasonqinzhou Sep 17, 2026
ef70c3d
Merge current main for PR 241 closeout
jasonqinzhou Sep 17, 2026
b44fda2
Merge current main for PR 250 closeout
jasonqinzhou Sep 17, 2026
0f63754
Merge current PR 250 base into nightly dispatch
jasonqinzhou Sep 17, 2026
5544497
fix(ci): record validated nightly source ref in provenance
jasonqinzhou Sep 17, 2026
31ef203
test(ci): enforce the scheduled nightly consumer boundary
jasonqinzhou Sep 17, 2026
8090664
Integrate approved PR 241 license checks before nightly skip fix
jasonqinzhou Sep 17, 2026
9bf86e2
Merge landed PR 241 from main for final validation
jasonqinzhou Sep 17, 2026
4181b16
Integrate PR 250 and landed PR 241 before final dispatch validation
jasonqinzhou Sep 17, 2026
0660cb1
fix(ci): compare nightly licenses only with scheduled baselines
jasonqinzhou Sep 17, 2026
44b3bf3
Merge PR 250 from main while preserving manual staging gates
jasonqinzhou Sep 17, 2026
7a30248
fix(ci): require current approval before manual compliance work
jasonqinzhou Sep 17, 2026
295c03a
Merge current main documentation before final nightly validation
jasonqinzhou Sep 17, 2026
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
112 changes: 99 additions & 13 deletions .github/workflows/nightly-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ on:
schedule:
# 08:00 UTC daily, matching the ai-dynamo org nightly convention.
- cron: "0 8 * * *"
# Manual runs build and stage only, gated by manual-release-approver.
# Dispatch from main so old release sources use the current workflow.
workflow_dispatch:
inputs:
commit_sha:
description: >-
Full commit SHA to build and stage; must be on main or release/*.
Dispatch from main. Empty builds main at dispatch time. No public publish.
required: false
default: ""
type: string

# Read-only default token; only the guard job adds actions:read.
permissions:
Expand All @@ -56,7 +67,7 @@ concurrency:

jobs:
changes-guard:
name: Skip when main is unchanged
name: Resolve target and skip unchanged nightlies
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
Expand All @@ -65,7 +76,58 @@ jobs:
outputs:
should-build: ${{ steps.decide.outputs.should-build }}
dev-date: ${{ steps.version.outputs.dev-date }}
target-sha: ${{ steps.target.outputs.sha }}
target-ref: ${{ steps.target.outputs.ref }}
steps:
# Runs with no secrets and no environment. Every manual target —
# including main's own tip — must lie on main or a release/* branch.
- name: Resolve and validate the target commit
id: target
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
REQUESTED_SHA: ${{ inputs.commit_sha }}
with:
script: |
if (context.eventName === 'schedule') {
core.setOutput('sha', context.sha);
core.setOutput('ref', context.ref);
return;
}
if (context.ref !== 'refs/heads/main') {
core.setFailed('Dispatch from main; use commit_sha to select release source');
return;
}
const requested = (process.env.REQUESTED_SHA || '').trim() || context.sha;
if (!/^[0-9a-f]{40}$/.test(requested)) {
core.setFailed('commit_sha must be a full 40-hex commit SHA');
return;
}
const branches = (await github.paginate(
github.rest.repos.listBranches, {...context.repo, per_page: 100}))
.map(b => b.name)
.filter(n => n === 'main' || n.startsWith('release/'));
for (const branch of branches) {
try {
const {data} = await github.rest.repos.compareCommitsWithBasehead({
...context.repo, basehead: `${requested}...${branch}`});
// ahead/identical: the branch tip contains the requested commit.
if (data.status === 'ahead' || data.status === 'identical') {
const {data: commit} = await github.rest.repos.getCommit(
{...context.repo, ref: requested});
core.notice(`Building ${requested} on ${branch}: ` +
`${commit.commit.committer.date} ` +
`"${commit.commit.message.split('\n')[0]}"`);
core.setOutput('sha', requested);
core.setOutput('ref', `refs/heads/${branch}`);
return;
}
} catch (error) {
// A missing comparison may mean the commit is unknown;
// authentication, rate-limit, and server failures are fatal.
if (error.status !== 404) throw error;
}
}
core.setFailed('commit_sha is not on main or any release/* branch');
# Computed once here so both arch legs stamp the same date even if the
# run crosses a UTC midnight.
- name: Compute nightly version date
Expand All @@ -77,8 +139,7 @@ jobs:
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
// Only scheduled runs dedup; any other event (e.g. a future
// workflow_dispatch) always builds.
// Only scheduled runs dedup; other events always build.
if (context.eventName !== 'schedule') {
core.setOutput('should-build', 'true');
return;
Expand All @@ -89,6 +150,8 @@ jobs:
repo: context.repo.repo,
workflow_id: 'nightly-ci.yml',
branch: 'main',
// Dispatch runs pin older SHAs; only prior crons may dedup.
event: 'schedule',
status: 'success',
per_page: 1,
});
Expand Down Expand Up @@ -133,8 +196,16 @@ jobs:
name: Python Dependency Licenses (${{ matrix.arch }}, ${{ matrix.python-version }})
# No release environment or build artifacts are available to dependency
# build hooks. Both architectures must pass before staging can start.
needs: changes-guard
if: needs.changes-guard.outputs.should-build == 'true'
needs: [changes-guard, manual-approval]
# Dependency installation uses production runners, so manual runs and
# retries need approval before even this isolated compliance work starts.
# !cancelled() allows the skipped approval ancestor on scheduled first runs.
if: >-
!cancelled() &&
needs.changes-guard.outputs.should-build == 'true' &&
((github.event_name == 'schedule' && github.run_attempt == '1') ||
(needs.manual-approval.result == 'success' &&
needs.manual-approval.outputs.approved-attempt == github.run_attempt))
strategy:
fail-fast: false
matrix:
Expand All @@ -148,10 +219,17 @@ jobs:
HOME: /__w/aisimulate/aisimulate
USER: runner
steps:
# Keep the license policy/tooling at the workflow revision. Older
# release sources need only their package manifest, not this helper.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ needs.changes-guard.outputs.target-sha }}
Comment thread
jasonqinzhou marked this conversation as resolved.
path: source
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: ${{ matrix.python-version }}
Expand All @@ -162,6 +240,7 @@ jobs:
run: |
python -m venv "${RUNNER_TEMP}/venv-licenses"
python scripts/check_python_licenses.py \
--pyproject source/python/aisimulate/pyproject.toml \
--python "${RUNNER_TEMP}/venv-licenses/bin/python" \
--inventory "${RUNNER_TEMP}/python-inventory/${MATRIX_ARCH}-${PYTHON_SERIES}.csv"
- name: Preserve the dependency inventory as data
Expand Down Expand Up @@ -238,6 +317,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.changes-guard.outputs.target-sha }}
persist-credentials: false

# HOME above is the checked-out workspace: anything written there
Expand Down Expand Up @@ -381,8 +461,8 @@ jobs:
MATRIX_RUNNER: ${{ matrix.runner }}
GH_REPOSITORY: ${{ github.repository }}
GH_WORKFLOW_REF: ${{ github.workflow_ref }}
GH_REF: ${{ github.ref }}
GH_SHA: ${{ github.sha }}
GH_REF: ${{ needs.changes-guard.outputs.target-ref }}
GH_SHA: ${{ needs.changes-guard.outputs.target-sha }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
GH_RUN_ID: ${{ github.run_id }}
GH_RUN_ATTEMPT: ${{ github.run_attempt }}
GH_EVENT_NAME: ${{ github.event_name }}
Expand Down Expand Up @@ -687,7 +767,7 @@ jobs:
return urllib.request.urlopen(req, timeout=30)
with get(
f"https://api.github.com/repos/{repo}/actions/workflows/"
"nightly-ci.yml/runs?branch=main&status=success&per_page=10") as response:
"nightly-ci.yml/runs?branch=main&event=schedule&status=success&per_page=10") as response:
runs = json.load(response)
for run in runs.get("workflow_runs", []):
if str(run["id"]) == current:
Expand Down Expand Up @@ -756,21 +836,25 @@ jobs:
# Refresh strict-native op-level FPE coverage at the exact nightly source
# SHA and with the same amd64 wheel verified against Artifactory.
# The GitLab publish trigger below requires this to succeed, and a failed
# refresh fails the run, so consumers never advance to it.
# refresh fails the scheduled run, so consumers never advance to it.
# Manual dispatch is build/stage-only; older release sources can lack this
# qualification harness. Release qualification has its own current harness
# in release-nightly-ci.yml and does not certify these manually staged wheels.
fpe-support-matrix:
name: FPE Support Matrix (main)
needs: [changes-guard, build-artifacts]
# !cancelled(): the implicit success() also inspects the transitively
# needed manual-approval job, which is skipped on scheduled runs.
if: >-
!cancelled() &&
github.event_name == 'schedule' &&
needs.changes-guard.outputs.should-build == 'true' &&
needs.build-artifacts.result == 'success'
permissions:
contents: read
uses: ./.github/workflows/fpe-support-matrix.yml
with:
expected_sha: ${{ github.sha }}
expected_sha: ${{ needs.changes-guard.outputs.target-sha }}
wheel_artifact: nightly-dist-amd64

# Security scanning runs in the internal GitLab release-automation
Expand All @@ -781,17 +865,19 @@ jobs:
# access exist; flip GITLAB_SECURITY_TRIGGER_ENABLED to activate.
trigger-gitlab-security:
name: Trigger GitLab security pipeline
needs: [manual-approval, build-artifacts, fpe-support-matrix, license-evidence]
needs: [changes-guard, manual-approval, build-artifacts, fpe-support-matrix, license-evidence]
# Same unattended-vs-approved split as build-artifacts, so re-running
# only this job cannot fire an unapproved publish. The FPE refresh must
# have succeeded before anything advances to the internal pipeline.
# Manual targets share the date-based version namespace, so they must
# never enter this public nightly publisher, even after approval.
if: >-
!cancelled() &&
needs.build-artifacts.result == 'success' &&
needs.fpe-support-matrix.result == 'success' &&
needs.license-evidence.result == 'success' &&
github.event_name == 'schedule' && github.ref == 'refs/heads/main' &&
((github.event_name == 'schedule' && github.run_attempt == '1') ||
(github.run_attempt == '1' ||
(needs.manual-approval.result == 'success' &&
needs.manual-approval.outputs.approved-attempt == github.run_attempt)) &&
vars.GITLAB_SECURITY_TRIGGER_ENABLED == 'true'
Expand Down Expand Up @@ -834,7 +920,7 @@ jobs:
GITLAB_PIPELINE_URL: ${{ secrets.GITLAB_PIPELINE_URL }}
SLACK_CHANNEL_ID: ${{ secrets.SLACK_RELEASE_CHANNEL_ID }}
GH_RUN_ID: ${{ github.run_id }}
GH_SHA: ${{ github.sha }}
GH_SHA: ${{ needs.changes-guard.outputs.target-sha }}
WHEEL_VERSION: ${{ needs.build-artifacts.outputs.wheel-version }}
run: |
set -euo pipefail
Expand Down
27 changes: 24 additions & 3 deletions docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ Manual dispatches and site-change triggers are listed below.
| --- | --- | --- |
| [Fast CI](../.github/workflows/fast-ci.yml) | PR open/update/reopen, ready-for-review and label changes; pushes to `main`, `release/*`, and trusted `pull-request/*`; manual dispatch with `expected_sha` | Quick checks and `Fast CI Success` |
| [Full CI](../.github/workflows/ci.yml) | Pushes to `main`, `release/*`, and trusted `pull-request/*`; manual dispatch with `expected_sha` | Selects and aggregates compiled validation |
| [Main branch nightly CI](../.github/workflows/nightly-ci.yml) | Daily at 08:00 UTC | Builds, qualifies, and stages nightly artifacts; skips rebuilding when `main` matches the last successful nightly |
| [Main branch nightly CI](../.github/workflows/nightly-ci.yml) | Daily at 08:00 UTC; manual dispatch from `main` | Scheduled runs build, qualify, and stage nightly artifacts, skipping unchanged `main`; manual runs build and stage an approved source SHA |
| [Validate platform wheels](../.github/workflows/validate-platform-wheels.yml) | Called by Full CI; manual dispatch | Linux x86-64/ARM64 and macOS ARM64 package validation |
| [Collector Data Check](../.github/workflows/collector-check.yml) | Called by Full CI; manual dispatch | Collector-data integrity and informational sanity reports |
| [Prediction Regression Gate](../.github/workflows/prediction-regression-gate.yml) | Called by Full CI; manual dispatch | Before/after prediction comparison |
Expand Down Expand Up @@ -319,7 +319,7 @@ timeout behavior. Local editable installs do not replace installed-wheel CI.

Main branch nightly CI builds the approved release surface: one `aisimulate` wheel per Linux
architecture and one `aisimulate-core` Rust source crate. A changes guard compares
`main` with the last successful nightly. The build stamps a date-based dev
`main` with the last successful scheduled nightly. The build stamps a date-based dev
version, uses pinned build tooling, and records checksums and provenance.

Python dependency licenses are checked in isolated jobs on both architectures
Expand All @@ -341,7 +341,7 @@ Two kinds of validation then run:
- **Wheel smoke tests:** fresh installations on amd64/arm64, each tested with
Python 3.11, 3.12, and 3.13; dependencies, package identity/version, imports,
and console commands are checked using the downloaded wheel.
- **FPE Support Matrix:** the amd64 nightly wheel is reused and checked against
- **FPE Support Matrix (scheduled runs):** the amd64 nightly wheel is reused and checked against
the expected source SHA and checksum. The installed SDK discovers live
system/backend combinations, then shards native `op_level` evaluation across
them. Qualification requires complete reports from the same wheel and the
Expand Down Expand Up @@ -402,6 +402,27 @@ trigger Pages; this job does not publish packages.
Release branches without retained qualified CI evidence appear unavailable.
See the [FPE publication contract](../python/aisimulate/docs/support-matrix/fpe.md#main-and-release-branches).

To build and stage a specific commit, dispatch from `main` and supply a full
40-character SHA reachable from `main` or a `release/*` branch:

```bash
gh workflow run nightly-ci.yml --ref main -f commit_sha=<full-source-sha>
```

An empty `commit_sha` selects `main` at dispatch time. Manual builds require
approval for the current run attempt, always build even when the source is
unchanged, and use current license-check tooling against the selected source's
package manifest. They run both architectures' wheel smoke tests and retain
checksums, source provenance, and license evidence. Staging paths include the
unique run ID, and manual runs neither block the scheduled concurrency group nor
count toward its unchanged-source guard.

Manual dispatches do not run FPE qualification or trigger the GitLab public
publisher. Their date-based package versions can match another build, so use
the exact run's `nightly-dist-<arch>` artifacts and checksums; these builds are
staging evidence, not a public or FPE-qualified nightly. Scheduled runs retain
the full FPE and public-publication gates above.

### E2E accuracy campaigns

The independent [E2E Accuracy Matrix](../.github/workflows/e2e-accuracy.yml)
Expand Down
7 changes: 4 additions & 3 deletions scripts/check_python_licenses.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
)


def check_licenses(python: str, inventory: Path | None = None) -> int:
with PYPROJECT.open("rb") as manifest:
def check_licenses(python: str, inventory: Path | None = None, pyproject: Path | None = None) -> int:
with (pyproject or PYPROJECT).open("rb") as manifest:
dependencies = tomllib.load(manifest)["project"]["dependencies"]
dependencies = [d for d in dependencies if not d.lower().startswith("aisimulate-core")]
with tempfile.TemporaryDirectory(prefix="aisimulate-licenses-") as temporary:
Expand Down Expand Up @@ -61,8 +61,9 @@ def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--python", default=sys.executable, help="Interpreter whose environment is checked")
parser.add_argument("--inventory", type=Path, help="Optional CSV output after the license check succeeds")
parser.add_argument("--pyproject", type=Path, help="Package manifest from the source revision being built")
args = parser.parse_args()
return check_licenses(args.python, args.inventory)
return check_licenses(args.python, args.inventory, args.pyproject)


if __name__ == "__main__":
Expand Down
Loading
Loading