diff --git a/.github/workflows/release-and-publish.yml b/.github/workflows/release-and-publish.yml index f4cd3fafa..bababcf87 100644 --- a/.github/workflows/release-and-publish.yml +++ b/.github/workflows/release-and-publish.yml @@ -5,6 +5,8 @@ name: Release and PyPI Publish # - build-and-attest: can only read source and issue artifact attestations; # - publish-pypi: receives only the project-scoped PyPI token; # - publish-github: may only attach assets to the GitHub Release. +# - reconcile-platform-manifest: updates main only after PyPI exposes the exact +# released bytes, then validates the complete platform contract. # # Every third-party action is pinned to a full commit SHA. Dependabot should # propose reviewed SHA updates rather than following mutable tags. @@ -155,14 +157,66 @@ jobs: github_token: ${{ github.token }} tag: ${{ needs.release.outputs.tag }} + reconcile-platform-manifest: + needs: [release, publish-pypi] + if: >- + ${{ needs.release.outputs.released == 'true' && + needs.publish-pypi.result == 'success' }} + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout released main + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + fetch-depth: 0 + token: ${{ secrets.ADMIN_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + + - name: Regenerate and validate the published platform manifest + run: | + for attempt in $(seq 1 12); do + if python scripts/generate_platform_manifest.py; then + break + fi + if [ "$attempt" -eq 12 ]; then + echo "PyPI did not expose the released launcher artifacts in time." >&2 + exit 1 + fi + echo "Published artifacts are not visible yet; retrying in 15 seconds." + sleep 15 + done + python scripts/validate_platform_manifest.py + python scripts/validate_platform_manifest.py --offline + + - name: Commit the reconciled manifest + run: | + if git diff --quiet -- platform-manifest.json; then + echo "platform-manifest.json already matches the published release." + exit 0 + fi + git config user.name "OpenAdapt Bot" + git config user.email "bot@openadapt.ai" + git add platform-manifest.json + git commit -m "chore(release): reconcile platform manifest" + git push origin HEAD:main + report-release-failure: - needs: [release, build-and-attest, publish-pypi, publish-github] + needs: + [release, build-and-attest, publish-pypi, publish-github, reconcile-platform-manifest] if: >- ${{ always() && (needs.release.result == 'failure' || needs.build-and-attest.result == 'failure' || needs.publish-pypi.result == 'failure' || - needs.publish-github.result == 'failure') }} + needs.publish-github.result == 'failure' || + needs.reconcile-platform-manifest.result == 'failure') }} runs-on: ubuntu-latest permissions: issues: write @@ -175,11 +229,12 @@ jobs: BUILD_RESULT: ${{ needs.build-and-attest.result }} PYPI_RESULT: ${{ needs.publish-pypi.result }} GITHUB_RESULT: ${{ needs.publish-github.result }} + MANIFEST_RESULT: ${{ needs.reconcile-platform-manifest.result }} run: | TITLE="Release workflow failed on main" BODY="The release workflow failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - release=${RELEASE_RESULT}, build-and-attest=${BUILD_RESULT}, publish-pypi=${PYPI_RESULT}, publish-github=${GITHUB_RESULT}. + release=${RELEASE_RESULT}, build-and-attest=${BUILD_RESULT}, publish-pypi=${PYPI_RESULT}, publish-github=${GITHUB_RESULT}, reconcile-platform-manifest=${MANIFEST_RESULT}. Until this is fixed, do not assume that the Git tag, PyPI files, GitHub assets, and attestations are mutually complete." EXISTING=$(gh issue list --repo "${{ github.repository }}" --state open --search "in:title \"$TITLE\"" --json number --jq '.[0].number // empty') diff --git a/docs/platform-manifest.md b/docs/platform-manifest.md index 688ae83b2..06da5f57e 100644 --- a/docs/platform-manifest.md +++ b/docs/platform-manifest.md @@ -36,7 +36,15 @@ it fails loudly. If a launcher release train is in flight (pyproject.toml ahead of PyPI), pass `--allow-unreleased-launcher`; the manifest still records the published version. -Regenerate the manifest after each component release and commit the result. +The launcher release workflow regenerates and validates the manifest after the +new launcher artifacts are visible on PyPI, then commits the exact published +URLs and digests back to `main`. This ordering is intentional: the manifest +cannot truthfully name a launcher release before its immutable artifacts exist. +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. ## How it is validated diff --git a/platform-manifest.json b/platform-manifest.json index 6cc7e209d..e282aa8e8 100644 --- a/platform-manifest.json +++ b/platform-manifest.json @@ -1,26 +1,26 @@ { "manifest_kind": "openadapt-platform-release-manifest", "schema_version": "1.0.0", - "generated_at": "2026-07-26T19:20:22+00:00", + "generated_at": "2026-07-26T20:55:04+00:00", "release_channel": "beta", "components": { "launcher": { "package": "openadapt", - "version": "1.7.3", + "version": "1.8.0", "source": "pypi", "requires_python": "<3.13,>=3.10", "artifacts": [ { "type": "bdist_wheel", - "filename": "openadapt-1.7.3-py3-none-any.whl", - "url": "https://files.pythonhosted.org/packages/08/58/990f7f31360b0be723e004271b1e7016708c21627b1910f3bef751e11f45/openadapt-1.7.3-py3-none-any.whl", - "sha256": "f173f1454802bc01c966f973755d6657af9b968cbc0318483d75751adc79b9d0" + "filename": "openadapt-1.8.0-py3-none-any.whl", + "url": "https://files.pythonhosted.org/packages/23/50/84edb7de7fb4046a1cfe4829a7bf65f392fcb1d7f547065dc733624b9c4d/openadapt-1.8.0-py3-none-any.whl", + "sha256": "4342fa0b48f790ee43b525fd440ccdfc6a4fa95b075d2c03eac695faa80c1cd5" }, { "type": "sdist", - "filename": "openadapt-1.7.3.tar.gz", - "url": "https://files.pythonhosted.org/packages/d1/f8/fed8b98bdc39376e7bd1aa9cccba22a415f54eb225b1f262d8126d9353d7/openadapt-1.7.3.tar.gz", - "sha256": "5a7d63bfacea0b579d06d48886348ce0bce38db7c55e04839ea9d860c36e6d79" + "filename": "openadapt-1.8.0.tar.gz", + "url": "https://files.pythonhosted.org/packages/a4/6b/f62cc892b1e37f28acf2824b8b5b9316e98e87aa6de303b1b6a8538bdccc/openadapt-1.8.0.tar.gz", + "sha256": "87d278e1be51f2b9b5a64fcbdd3f7ad33097c135c9dfdb916c529cd77f5eeb80" } ] },