Skip to content

0.2.1 — the image on Python 3.14 #4

0.2.1 — the image on Python 3.14

0.2.1 — the image on Python 3.14 #4

Workflow file for this run

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