0.2.1 — the image on Python 3.14 #4
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish | |
| # Publishes the release artefacts when a GitHub release is published: | |
| # * the sdist and the wheel to PyPI, through Trusted Publishing | |
| # * the container image to ghcr.io, through the automatic GITHUB_TOKEN | |
| # | |
| # The two jobs are independent, so a broken image push does not withhold the | |
| # upload to PyPI and neither waits on the other. | |
| # | |
| # `workflow_dispatch` runs the image half alone and tags the result `edge`. | |
| # PyPI is skipped there: a version may be uploaded to PyPI exactly once, and a | |
| # manual run carries no release tag it could legitimately claim a version from. | |
| # | |
| # Authentication is Trusted Publishing, which exchanges the workflow's OIDC | |
| # identity for a short-lived token. **No API token is stored anywhere in this | |
| # repository**, which matters for a project whose whole subject is not | |
| # leaving credentials lying around. | |
| # | |
| # One-time setup on PyPI, by the account that will own the project. Under | |
| # "Publishing" add a pending publisher with exactly: | |
| # | |
| # PyPI project name: benethos-lexware-office-mcp | |
| # Owner: benethos-hub | |
| # Repository: lexware-office-mcp | |
| # Workflow filename: publish.yml | |
| # Environment: pypi | |
| # | |
| # All five have to match or the upload is refused, and the environment below | |
| # is the one it names. | |
| # | |
| # The registry needed no setup, measured on 2026-08-22 with the first push: | |
| # the package came out public and was pulled with no login and no account, | |
| # even though the documented behaviour is that a new package is private until | |
| # switched. If a pull ever does come back unauthorized, that switch is under | |
| # Packages -> Package settings -> Change visibility. | |
| # | |
| # The release process itself is in SPECS.md section 16. | |
| on: | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| pypi-publish: | |
| name: Build and publish to PyPI | |
| # A manual run has no version to publish, and PyPI refuses a re-upload. | |
| if: github.event_name == 'release' | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/p/benethos-lexware-office-mcp | |
| permissions: | |
| id-token: write # Trusted Publishing, in place of a stored token | |
| steps: | |
| - uses: actions/checkout@v7 | |
| # No `enable-cache` on purpose. The action's default disables the cache | |
| # for a `release` event, which is exactly this job: it holds the OIDC | |
| # identity that uploads to PyPI, and a restored cache is one more thing | |
| # that would have to be trusted. It installs almost nothing anyway. | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v10.0.1 | |
| - name: Build sdist and wheel | |
| run: uv build | |
| # The wheel is what a user installs, and it has been wrong before: the | |
| # settings sample lived beside the package and was never packed. Assert | |
| # it is in there rather than finding out from a bug report. | |
| - name: The wheel carries what it is supposed to | |
| run: | | |
| python - <<'PY' | |
| import pathlib, zipfile | |
| wheel = next(pathlib.Path("dist").glob("*.whl")) | |
| names = zipfile.ZipFile(wheel).namelist() | |
| expected = [ | |
| "benethos_lexware_office_mcp/env.sample", | |
| "benethos_lexware_office_mcp/py.typed", | |
| "benethos_lexware_office_mcp/server.py", | |
| ] | |
| missing = [n for n in expected if n not in names] | |
| assert not missing, f"{wheel.name} is missing {missing}" | |
| print(f"OK: {wheel.name}, {len(names)} files") | |
| PY | |
| - name: Publish to PyPI (Trusted Publishing) | |
| run: uv publish --trusted-publishing always | |
| ghcr-publish: | |
| name: Build and publish the image | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write # push to ghcr.io as the GITHUB_TOKEN identity | |
| steps: | |
| - uses: actions/checkout@v7 | |
| # arm64 is emulated on an amd64 runner. Nothing in this image is compiled | |
| # from source - pypdfium2 ships a prebuilt PDFium for aarch64 and the rest | |
| # is pure Python - so the emulation only has to run an unpack. | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v4 | |
| - name: Set up Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to ghcr.io | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Derive image tags, labels and annotations | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| env: | |
| # Annotate the index as well as the individual manifests. The default | |
| # is manifests only, and for a multi-architecture image the package | |
| # page reads the index - the manifest list above the per-architecture | |
| # manifests. Labels sitting in the image configuration are then | |
| # perfectly present and read by nothing, which is exactly how a | |
| # package page ends up saying "No description provided". | |
| DOCKER_METADATA_ANNOTATIONS_LEVELS: index,manifest | |
| with: | |
| images: ghcr.io/${{ github.repository }} | |
| flavor: latest=false | |
| tags: | | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=raw,value=latest,enable=${{ github.event_name == 'release' }} | |
| type=raw,value=edge,enable=${{ github.event_name == 'workflow_dispatch' }} | |
| - name: Build and push | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| annotations: ${{ steps.meta.outputs.annotations }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| # Read the published index back. Setting the annotations above and | |
| # trusting them is what produced a description nobody could see, and the | |
| # cost of finding out is a release that has already happened. | |
| - name: The published index carries what the package page reads | |
| run: | | |
| ref=$(printf '%s\n' "${{ steps.meta.outputs.tags }}" | head -1) | |
| echo "inspecting $ref" | |
| docker buildx imagetools inspect "$ref" --raw > /tmp/index.json | |
| python3 - <<'PY' | |
| import json | |
| index = json.load(open("/tmp/index.json")) | |
| annotations = index.get("annotations") or {} | |
| platforms = sorted( | |
| f"{m['platform']['os']}/{m['platform']['architecture']}" | |
| for m in index.get("manifests", []) | |
| if m.get("platform", {}).get("architecture") != "unknown" | |
| ) | |
| missing = [ | |
| key | |
| for key in ( | |
| "org.opencontainers.image.description", | |
| "org.opencontainers.image.source", | |
| "org.opencontainers.image.version", | |
| ) | |
| if not annotations.get(key) | |
| ] | |
| assert not missing, f"the index carries no {missing} - the package page shows nothing" | |
| assert "linux/amd64" in platforms, f"amd64 is missing, index holds {platforms}" | |
| assert "linux/arm64" in platforms, f"arm64 is missing, index holds {platforms}" | |
| print("OK:", ", ".join(platforms)) | |
| print(" ", annotations["org.opencontainers.image.description"]) | |
| PY |