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
69 changes: 66 additions & 3 deletions .github/workflows/platform-manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,25 @@ on:
branches:
- main
schedule:
# Weekly drift check: a component release without a manifest regeneration
# should fail here, loudly, even if no PR touches this repo.
- cron: '17 6 * * 1'
# DAILY drift check. The manifest goes stale because openadapt-flow,
# -capture, or -desktop released in ANOTHER repository, so no commit
# lands here and only the schedule can notice. This was weekly, which
# meant up to seven days of serving wrong sha256 digests from
# raw.githubusercontent; flow 1.24.0 published on 2026-07-27 and the
# manifest still advertised 1.23.0's digests. One small stdlib-only job
# costs seconds a day.
- cron: '17 6 * * *'
workflow_dispatch:
# Receiver for a component repository to announce its release immediately
# rather than waiting for the daily sweep. The sending side is a one-step
# `gh api repos/OpenAdaptAI/OpenAdapt/dispatches -f event_type=component-released`
# in openadapt-flow/-capture/-desktop's release workflow.
repository_dispatch:
types: [component-released]

concurrency:
group: platform-manifest-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read
Expand All @@ -32,9 +47,57 @@ jobs:
with:
python-version: '3.12'

# Cheapest decisive check first: structure, signature honesty, and
# repo agreement need no network and cannot be flaky.
- name: Validate platform manifest structure (offline)
run: python scripts/validate_platform_manifest.py --offline

# Standard library only: no dependency install, no lockfile, no cache.
- name: Validate platform manifest against published artifacts
run: python scripts/validate_platform_manifest.py

- name: Prove the drift guard fails when it should
# The daily/dispatched production check is intentionally stdlib-only.
# This characterization suite belongs on source changes, not every
# remote drift probe, so scheduled checks stay fast and network-light.
if: github.event_name == 'pull_request' || github.event_name == 'push'
run: |
python -m pip install --quiet 'pytest>=8.0.0'
python -m pytest tests/test_platform_manifest_drift.py -q

report-manifest-drift:
# A red scheduled run in a repository nobody has open is not a signal.
# The 2026-07-27 drift DID turn the cron red and still shipped, because
# main was already red from the previous night's release. File an issue
# so drift has an owner and a paper trail instead of a stale red dot.
needs: validate-platform-manifest
if: >-
${{ always() && needs.validate-platform-manifest.result == 'failure' &&
(github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
github.event_name == 'repository_dispatch') }}
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: File or update the manifest drift issue
env:
GH_TOKEN: ${{ github.token }}
run: |
TITLE="platform-manifest.json has drifted from the published releases"
BODY="The scheduled platform manifest check failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}

\`platform-manifest.json\` is served publicly from raw.githubusercontent and is OpenAdapt's only cryptographic claim about the current release. While it is stale, anyone verifying a downloaded wheel or sdist against it fails.

Fix: \`python scripts/generate_platform_manifest.py && python scripts/validate_platform_manifest.py\`, then open a PR."
EXISTING=$(gh issue list --repo "${{ github.repository }}" --state open \
--search "in:title \"$TITLE\"" --json number --jq '.[0].number // empty')
if [ -n "$EXISTING" ]; then
gh issue comment "$EXISTING" --repo "${{ github.repository }}" --body "$BODY"
else
gh issue create --repo "${{ github.repository }}" --title "$TITLE" --body "$BODY"
fi

check-source-boundary:
runs-on: ubuntu-latest
steps:
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/release-and-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,10 @@ jobs:
echo "Published artifacts are not visible yet; retrying in 15 seconds."
sleep 15
done
python scripts/validate_platform_manifest.py
# --require-network: at the release gate an unreachable PyPI must
# fail. Elsewhere it only warns, because an index outage is not
# evidence of drift and a flaky guard stops being read.
python scripts/validate_platform_manifest.py --require-network
python scripts/validate_platform_manifest.py --offline

- name: Commit the reconciled manifest
Expand Down
49 changes: 43 additions & 6 deletions docs/platform-manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,47 @@ If publication or reconciliation fails, `main` remains red and the release
workflow opens or updates a failure issue rather than weakening validation.

Regenerate and commit the manifest manually after other component releases;
the scheduled validator catches component or public-status drift.
the daily scheduled validator catches component or public-status drift.

## How it is validated

```bash
python scripts/validate_platform_manifest.py
```

CI runs this on every pull request and on a schedule
CI runs this on every pull request, on pushes to `main`, and DAILY
(`.github/workflows/platform-manifest.yml`), so drift between the committed
manifest and the actually published artifacts fails loudly.
manifest and the actually published artifacts fails loudly. A component
repository can also trigger the check immediately via a
`component-released` `repository_dispatch`.

### Why the schedule is daily and why false reds are not tolerated

This manifest went stale once, and both causes are worth stating because they
are properties of the check rather than of the manifest:

1. **The staleness originates in another repository.** `openadapt-flow`
released 1.24.0 on 2026-07-27; nothing was committed here, so only the
schedule could notice. It was weekly, so the public raw.githubusercontent
copy could advertise superseded sha256 digests for up to seven days.
The schedule is now daily and the job is stdlib-only (no install, no cache),
costing seconds a day.
2. **The check failed benignly after every release, so a real failure was
invisible.** Two conditions were classified as errors when they are normal:
the semantic-release version commit leaves `pyproject.toml` ahead of the
not-yet-reconciled manifest, and PyPI's `info.version` lags an upload by
minutes. Both now warn, while every digest, URL, and filename comparison
stays fatal. A guard that cries wolf at every release gets ignored, and
that is exactly what happened.

A failed scheduled or dispatched run also files (or comments on) a
`platform-manifest.json has drifted` issue, so drift has an owner rather than
a stale red dot on a repository nobody has open.

`tests/test_platform_manifest_drift.py` proves the guard FAILS on a simulated
future release, a tampered digest, a tampered URL, and a version PyPI never
published — and that it does NOT fail on the two transient release-time
conditions above.

Validated today:

Expand All @@ -63,10 +93,17 @@ Validated today:
- Signature honesty: while `signature.value` is null, `signature.status` must
read `unsigned (signing infrastructure pending)`. A non-null signature
value fails validation because no verification path exists yet.
- Repo agreement: launcher version and openadapt-* compatibility ranges match
`pyproject.toml`.
- Repo agreement: openadapt-* compatibility ranges match `pyproject.toml`, and
the manifest's launcher version is not ahead of `pyproject.toml`'s. A
`pyproject.toml` ahead of the manifest is the normal in-flight-release state
and only warns.
- Published-artifact agreement: component versions, artifact filenames, URLs,
and sha256 digests match PyPI exactly (`--offline` skips this class).
and sha256 digests match PyPI exactly (`--offline` skips this class). A
manifest version behind PyPI's latest, or naming a release PyPI never
published, fails; one ahead of `info.version` but already present in
`releases` is index propagation lag and warns, with digests still verified.
An unreachable PyPI warns rather than fails — it is not evidence of drift —
unless `--require-network` is passed.
- Status skew: disagreement with `status.json` versions is a warning by
default (status.json regenerates on its own cadence in `openadapt-web`);
`--strict-status` escalates it to a failure.
Expand Down
16 changes: 8 additions & 8 deletions platform-manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_kind": "openadapt-platform-release-manifest",
"schema_version": "1.0.0",
"generated_at": "2026-07-26T23:59:24+00:00",
"generated_at": "2026-07-27T14:10:57+00:00",
"release_channel": "beta",
"components": {
"launcher": {
Expand All @@ -26,21 +26,21 @@
},
"flow": {
"package": "openadapt-flow",
"version": "1.23.0",
"version": "1.24.0",
"source": "pypi",
"requires_python": "<3.13,>=3.10",
"artifacts": [
{
"type": "bdist_wheel",
"filename": "openadapt_flow-1.23.0-py3-none-any.whl",
"url": "https://files.pythonhosted.org/packages/54/f7/632b13c60ba805cfd22614dc4c73285c06c2b7d698c124d9c45104623c18/openadapt_flow-1.23.0-py3-none-any.whl",
"sha256": "b5d10dfc294479866d6dddd4c1a6afc9164414f0634703173b7e440dc0133bec"
"filename": "openadapt_flow-1.24.0-py3-none-any.whl",
"url": "https://files.pythonhosted.org/packages/12/fb/86a58376c9bb7e587c45f3d1a149486053efb0ae86069b46d7581696983a/openadapt_flow-1.24.0-py3-none-any.whl",
"sha256": "170fdac154794292c99dc6eea6486e7a2c3fdf321bcd87976d924bccd3db4aef"
},
{
"type": "sdist",
"filename": "openadapt_flow-1.23.0.tar.gz",
"url": "https://files.pythonhosted.org/packages/f6/e5/8957790965a0fc5cde4e8f8dc8664c3db682d1b09881abf05f013fd3f495/openadapt_flow-1.23.0.tar.gz",
"sha256": "b16e181ac1f8b84f825b418ca9c6608cd408bde9ccd94d0a7e62db515d2145be"
"filename": "openadapt_flow-1.24.0.tar.gz",
"url": "https://files.pythonhosted.org/packages/4c/db/5b570f6511bcaed68134be094e9fc2b745f4f77c3cadf13a5d7518ac883f/openadapt_flow-1.24.0.tar.gz",
"sha256": "2d4702e5ccbdfed0f78063ca510dc82894a2833edbed71d77edffbd0ffebd67d"
}
]
},
Expand Down
Loading