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
100 changes: 100 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ jobs:
permissions:
contents: write
id-token: write
# The pin bump below opens a PR rather than pushing to main.
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down Expand Up @@ -69,3 +71,101 @@ jobs:
--title "$TAG" \
--generate-notes \
--verify-tag

# `BACKEND_NPM_VERSION` is the backend a `pip install` user actually runs,
# and nothing derives it: the Python adapter is not in the pnpm workspace,
# so changesets never sees it. Left to a human it is remembered late —
# `check_backend_pin.py` then fails every Python CI run and blocks the
# PyPI publish until someone edits one line.
#
# Opened as a PR, never pushed to main: the new backend has to be ON npm
# for the check to pass, which is why this runs after Publish, and a pin
# is a claim about a published artifact that deserves a review.
#
# A PR raised with GITHUB_TOKEN gets no CI — GitHub suppresses workflow
# runs for events its own token raises, to stop recursion. Set
# PIN_BUMP_TOKEN (a PAT or App token) and the checks run on their own;
# without it the PR needs a nudge, which its body says.
#
# Only for a `latest` release: the pin is what a `pip install` user gets,
# and a `next` publish is a test version nobody should be moved onto.
- name: Open the Python backend pin bump
if: inputs.distTag == 'latest' || inputs.distTag == ''
env:
GITHUB_TOKEN: ${{ secrets.PIN_BUMP_TOKEN || secrets.GITHUB_TOKEN }}
PIN_FILE: packages/selenium-devtools-py/src/selenium_devtools/constants.py
run: |
set -euo pipefail
BACKEND=$(node -p "require('./packages/backend/package.json').version")
PIN=$(sed -n 's/^BACKEND_NPM_VERSION = "\(.*\)"$/\1/p' "$PIN_FILE")
if [ -z "$PIN" ]; then
echo "::error::No BACKEND_NPM_VERSION found in $PIN_FILE"
exit 1
fi
if [ "$BACKEND" = "$PIN" ]; then
echo "Backend unchanged at $PIN — nothing to bump."
exit 0
fi
# This value reaches a `sed` expression and a branch name. It comes
# from our own package.json, so this is a belt rather than a fix for
# a known hole — but a stray `/` would silently corrupt the pin.
case "$BACKEND" in
''|*[!0-9A-Za-z.+-]*)
echo "::error::Backend version is not plain semver: $BACKEND"
exit 1
;;
esac

# Only ever forward. A release that rolls the backend BACK — a revert
# on main, or a republish of an older `latest` — would otherwise open
# a PR lowering the pin onto a backend that may not serve the
# contract; `check_backend_pin.py` would reject it, after the noise.
NEWER=$(printf '%s\n%s\n' "$PIN" "$BACKEND" | sort -V | tail -1)
if [ "$NEWER" = "$PIN" ]; then
echo "Published backend $BACKEND is older than the pin $PIN — leaving it alone."
exit 0
fi

BRANCH="chore/pin-backend-$BACKEND"
FRAGMENT="packages/selenium-devtools-py/changes/pin-backend-$BACKEND.md"
# Continue the branch if it is already out there rather than recreating
# it: this step runs after `changeset publish`, so a re-run is normal,
# and by then a reviewer may have pushed to the open PR — regenerating
# `_contract.py` is the expected reason. A force push would erase that
# work, so there is none: we branch FROM the remote when it exists and
# fast-forward it.
git fetch --quiet origin "$BRANCH" 2>/dev/null || true
if git rev-parse --verify --quiet "origin/$BRANCH" >/dev/null; then
git switch -c "$BRANCH" "origin/$BRANCH"
else
git switch -c "$BRANCH"
fi

sed -i "s/^BACKEND_NPM_VERSION = .*/BACKEND_NPM_VERSION = \"$BACKEND\"/" "$PIN_FILE"
# Required, not decorative: python.yml refuses a branch that changes
# `src/` and documents nothing, so a pin-only PR would fail its own CI.
printf -- '---\npatch\n---\n\nPin the backend a `pip install` user runs to %s.\n' \
"$BACKEND" > "$FRAGMENT"
# Named explicitly because the fragment is UNTRACKED: `commit -a`
# stages modified tracked files only, so it would ship the pin without
# the fragment that makes the pin mergeable.
git add "$PIN_FILE" "$FRAGMENT"
if git diff --cached --quiet; then
echo "Branch $BRANCH already carries this bump — nothing to push."
else
git commit -m "chore(python): pin the backend to $BACKEND"
git push origin "$BRANCH"
fi

if [ "$(gh pr list --head "$BRANCH" --state open --json number --jq 'length')" != "0" ]; then
echo "A pin PR for $BACKEND is already open — left as it is."
exit 0
fi
gh pr create --base main --head "$BRANCH" \
--title "chore(python): pin the backend to $BACKEND" \
--body "$(printf '%s\n' \
"\`@wdio/devtools-backend@$BACKEND\` is published, so the version a \`pip install\` user runs can move up from $PIN." \
"" \
"Opened by the npm release. \`scripts/check_backend_pin.py\` is what decides whether this pin is serviceable — it asks the published tarball whether it carries every contract name the adapter needs, so let that check run before merging." \
"" \
"If this PR shows no checks, it was raised with \`GITHUB_TOKEN\`, which GitHub will not let start a workflow. Close and reopen it, or push an empty commit, to get CI." )"
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ Two mechanisms, and the Python one exists because the npm one cannot reach it. C

Neither is hand-versioned: both assemble the version and the changelog at release. The Python release additionally consumes its fragments, bumps `__version__` (the single source — `pyproject.toml` reads it via `dynamic = ["version"]`), and tags `py-v<version>` **after** a successful publish, so the tag is an output pointing at the published tree rather than an input naming a version nothing has computed yet.

`BACKEND_NPM_VERSION` is the backend a `pip install` user actually runs, so the npm release goes first; `release.yml` opens the pin bump as a PR, and `scripts/check_backend_pin.py` refuses a PyPI publish whose pinned backend cannot serve the contract.
`BACKEND_NPM_VERSION` is the backend a `pip install` user actually runs, so the npm release goes first; `release.yml` opens the pin bump as a PR, and `scripts/check_backend_pin.py` refuses a PyPI publish whose pinned backend cannot serve the contract. That PR carries its own `changes/` fragment, because `python.yml` refuses a branch that changes `src/` and documents nothing — a pin-only PR would fail its own CI. It is a PR rather than a push because the pin is a claim about a *published* artifact and `check_backend_pin.py` is what adjudicates it; merging one queues a `patch` for the next PyPI release rather than bumping `__version__` there and then. Raised with `GITHUB_TOKEN` it arrives with **no checks at all** — GitHub suppresses workflow runs for events its own token raises — so either set `PIN_BUMP_TOKEN` or close/reopen the PR to get CI onto it.

### Commits

Expand Down
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ The frontmatter is the bump level alone (`patch`, `minor`, `major`); the body is
python3 packages/selenium-devtools-py/scripts/changes.py next-version # what a release would publish
```

**You don't bump `BACKEND_NPM_VERSION`.** That constant names the backend a
`pip install` user actually runs, so it can only move once that backend is on
npm — the npm release opens the bump as its own PR, fragment included. If a
Python CI run fails on *"Pinned backend serves this adapter's contract"*, the
answer is that the backend has not been released yet, not that your branch is
wrong. Raise it by hand only when you are also releasing the backend.

## Before you push

- `pnpm build`, `pnpm test`, and `pnpm lint` all green — don't push red.
Expand Down
5 changes: 5 additions & 0 deletions packages/selenium-devtools-py/changes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ At release, `scripts/changes.py apply` takes the highest level of all pending
files, bumps `__version__`, writes the section into `CHANGELOG.md`, and deletes
the files it consumed. CI refuses a pull request that changes `src/` without
adding one.

One of these is written for you: the npm release opens a PR bumping
`BACKEND_NPM_VERSION` to the backend it just published, and that PR brings a
`patch` fragment with it — `constants.py` is under `src/`, so without one the
bump would fail the very check that exists to keep this package documented.
Loading