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: 68 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
name: Publish

# Publishes to PyPI when a version tag is pushed, so people can
# `pipx install fxcss` instead of cloning.
# `pipx install fxcss` instead of cloning. The Release workflow tags a release
# when its "Prepare vX.Y.Z" pull request merges and starts this run on the tag.
#
# Both tag pushes and manual runs must pass the same unit and Firefox smoke
# tests as pull requests. A tag must also match the built package's version.
#
# Once PyPI serves the release, the follow-up job moves the README's pins to
# it, refreshes fxcss.com and creates the GitHub release if there isn't one.
#
# It uses PyPI Trusted Publishing, so there is no API token to store. Configure
# the publisher once at https://pypi.org/manage/account/publishing/ with:
# owner AdamXweb · repo fxcss · workflow publish.yml · environment pypi
Expand All @@ -28,6 +32,8 @@ jobs:
# try to publish to the upstream project's name.
if: github.repository == 'AdamXweb/fxcss'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
Expand Down Expand Up @@ -58,6 +64,9 @@ jobs:
f"package version {version!r}; expected v{version}")
print(f"Validated fxcss {version} ({ref_type}: {ref_name})")
PY
- name: Record the version
id: version
run: echo "version=$(basename dist/*.whl | cut -d- -f2)" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v7
with:
name: dist
Expand Down Expand Up @@ -105,3 +114,61 @@ jobs:
done
echo "::error::fxcss==$version still not installable 10 minutes after upload — do not bump pins off this run"
exit 1

# Runs only after the gate above, so nothing here advertises an install that
# fails. A push made with GITHUB_TOKEN starts no workflow, so this starts the
# website refresh itself rather than relying on Website docs. The later
# steps still run if an earlier one fails; the job then fails to say so.
follow-up:
needs: [build, publish]
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write # push the pin update and create the release
actions: write # start Refresh docs
steps:
- uses: actions/checkout@v7
with:
ref: main
- name: Move the README pins
env:
VERSION: ${{ needs.build.outputs.version }}
run: |
set -euo pipefail
python3 scripts/release.py bump-pins "$VERSION" README.md
if git diff --quiet -- README.md; then
exit 0
fi
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git commit -q -m "Update the README pins to fxcss $VERSION" -- README.md
for _ in 1 2 3; do
if git push origin HEAD:main; then
echo "::notice title=README pins::Moved to fxcss $VERSION ($(git rev-parse --short HEAD))."
exit 0
fi
git pull -q --rebase origin main
done
echo "::error title=README pins::Could not push the pin update to main."
exit 1
- name: Refresh the website
if: ${{ !cancelled() }}
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run refresh-docs.yml --repo "$GITHUB_REPOSITORY" --ref website
- name: Create the GitHub release
if: ${{ !cancelled() }}
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.build.outputs.version }}
run: |
set -euo pipefail
if gh release view "v$VERSION" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then
echo "v$VERSION already has a release."
exit 0
fi
# --target only applies when the tag doesn't exist yet: a manual run
# from a branch then tags the commit it published.
gh release create "v$VERSION" --repo "$GITHUB_REPOSITORY" \
--target "$GITHUB_SHA" --title "v$VERSION" --generate-notes
echo "::notice title=GitHub release::Created v$VERSION. Edit its notes on the releases page."
30 changes: 30 additions & 0 deletions .github/workflows/release-version.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Release version

# Merging a change to fxcss.__version__ releases that version (see the Release
# workflow), so only a release pull request may make one. Its title must say
# "Prepare vX.Y.Z" for the new version, the version must increase, and it must
# not already be tagged. Pull requests that leave the version alone pass.
#
# It also runs when a pull request is retitled, so fixing the title clears it.

on:
pull_request:
types: [opened, edited, synchronize, reopened]

permissions:
contents: read

jobs:
version:
name: Check the version change
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Check the version change
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_TITLE: ${{ github.event.pull_request.title }}
run: python3 scripts/release.py check-pr "$BASE_SHA"
75 changes: 75 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Release

# Tags a release when its "Prepare vX.Y.Z" pull request merges, then starts
# Publish on that tag. Publish uploads to PyPI, waits until the release is
# installable, then moves the README's pins, refreshes fxcss.com and creates
# the GitHub release.
#
# A tag pushed with GITHUB_TOKEN starts no workflow, so this starts Publish
# itself. The Release version check stops other pull requests from changing
# __version__; if one is merged anyway, this run fails instead of releasing it.
#
# Run it by hand to release the version already on main:
# gh workflow run release.yml --ref main

on:
push:
branches: [main]
paths: [fxcss/__init__.py]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: release
cancel-in-progress: false

jobs:
tag:
if: github.repository == 'AdamXweb/fxcss' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write # push the tag
actions: write # start Publish
pull-requests: read # find the pull request this commit came from
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Find the release pull request
id: pr
if: github.event_name == 'push'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
title=$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls" \
--jq '[.[] | select(.merged_at != null)][0].title // ""')
printf 'Pull request: %s\n' "${title:-none}"
# A pull request title is a single line.
printf 'title=%s\n' "$title" >> "$GITHUB_OUTPUT"
- name: Decide whether to release
id: plan
env:
EVENT_NAME: ${{ github.event_name }}
PR_TITLE: ${{ steps.pr.outputs.title }}
run: python3 scripts/release.py plan
- name: Tag the release
if: steps.plan.outputs.release == 'yes'
env:
VERSION: ${{ steps.plan.outputs.version }}
run: |
set -euo pipefail
git tag "v$VERSION" "$GITHUB_SHA"
git push origin "refs/tags/v$VERSION"
- name: Start Publish on the tag
if: steps.plan.outputs.release == 'yes'
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.plan.outputs.version }}
run: |
set -euo pipefail
gh workflow run publish.yml --repo "$GITHUB_REPOSITORY" --ref "v$VERSION"
echo "::notice title=Release::Tagged v$VERSION and started Publish."
16 changes: 6 additions & 10 deletions .github/workflows/website-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ name: Website docs
# fxcss.com's documentation is generated from this README, and its homepage
# shows the released version. The site lives on the `website` branch, whose
# Refresh docs workflow regenerates it from main, checks it and publishes it.
# This starts that workflow whenever one of its inputs changes:
# - a release reaches PyPI (Publish succeeded, including its
# installability gate), which moves the version;
# - README.md changes on main, such as the pin update after a release.
# This starts that workflow whenever README.md changes on main.
#
# After a release, Publish starts it instead, once PyPI serves the new version
# and the README's pins have moved to it. That pin update is pushed with
# GITHUB_TOKEN, which starts no workflow, so it doesn't run this one too.
# The refresh skips a version PyPI doesn't serve yet, so README changes merged
# between a release's prep and its upload don't advertise it early.

on:
workflow_run:
workflows: [Publish]
types: [completed]
push:
branches: [main]
paths: [README.md]
Expand All @@ -23,9 +21,7 @@ permissions:

jobs:
refresh:
if: >-
github.repository == 'AdamXweb/fxcss' &&
(github.event_name == 'push' || github.event.workflow_run.conclusion == 'success')
if: github.repository == 'AdamXweb/fxcss'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
Expand Down
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1502,7 +1502,7 @@ text.
For a custom workflow, the core capture-and-compare steps are:

```yaml
- run: pip install "fxcss[images]==0.24.0"
- run: pip install "fxcss[images]==0.25.0"
- run: fxcss shot --theme base --out shots/base
- run: fxcss shot --theme head --out shots/head
- run: fxcss compare --base shots/base --head shots/head --out out/ --platform ${{ runner.os }}
Expand Down Expand Up @@ -1657,7 +1657,7 @@ the [releases page](https://github.com/AdamXweb/fxcss/releases) has the latest.
CI runners' Pythons are not externally managed, so plain pip is fine there:

```bash
pip install "fxcss[images]==0.24.0"
pip install "fxcss[images]==0.25.0"
```

Upgrade an existing pipx installation with `pipx upgrade fxcss`.
Expand Down Expand Up @@ -1864,7 +1864,37 @@ workflow tests also use Node and bash when they are installed, and skip
otherwise. Offline release orchestration tests execute the workflow scripts
with stubbed pip, sleep and gh commands to check tag validation, the PyPI
installability retry gate and website dispatch without network access. They
also check that publication still requires CI and the built-distribution checks.
also check that publication still requires CI and the built-distribution checks,
and run the release rules, tagging and the pin update against local Git
repositories.

### Releasing fxcss

A release is one pull request. Title it **Prepare vX.Y.Z** and change
`__version__` in `fxcss/__init__.py` to X.Y.Z. Merging it does the rest:

1. **Release** tags the merge commit `vX.Y.Z` and starts **Publish** on that tag.
2. **Publish** runs CI, checks the built packages, uploads them to PyPI and
waits until pip can install the new version.
3. Publish then moves the README's `fxcss==` pins to the new version, refreshes
fxcss.com and creates the GitHub release with generated notes. You can edit
the notes afterwards.

Only a release pull request may change `__version__`. The **Release version**
check fails any other pull request that does, and Release won't tag a version
that didn't come from one. While features land, leave the version at the
latest release and write "X.Y.Z or later" in the README where a feature needs
the next one.

To release the version that is already on main, run Release by hand:

```bash
gh workflow run release.yml --ref main
```

If Publish fails, re-run it from the Actions tab. If the fix needs a code
change, merge the fix, delete the tag with `git push origin :refs/tags/vX.Y.Z`,
then run Release by hand.

## How this was built

Expand Down
Loading
Loading