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
12 changes: 12 additions & 0 deletions .copier-answers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
_commit: d9d0a6f
_src_path: gh:bedrock-python/python-library-template
author_email: shalaevad.alexey@gmail.com
author_name: Alexey Shalaev
github_org: bedrock-python
initial_version: 0.0.0
package_name: mattermind
project_description: Ask questions about your Mattermost workspace using natural language
project_name: mattermind
project_slug: mattermind
python_min_version: '3.12'
17 changes: 14 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,28 @@ jobs:
- run: uv sync --extra dev
- run: uv run pytest -m unit --tb=short --cov=src/mattermind --cov-report=term --cov-fail-under=80

# Sentinel job — use this as the single required status check in branch protection.
# Sentinel job — the single required status check in the master ruleset. A job
# counts only when it succeeded: "skipped" means the work did not happen. A job
# skipped by design (one that runs on pull requests only, say) goes in MAY_SKIP
# by its job id, and only those may end skipped.
all-checks-passed:
name: All checks passed
if: always()
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- name: Check all jobs succeeded
env:
NEEDS: ${{ toJSON(needs) }}
MAY_SKIP: '[]'
run: |
if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more required checks failed."
NOT_PASSED=$(jq -r --argjson skip "$MAY_SKIP" '
to_entries[]
| select((.value.result == "success" or (.value.result == "skipped" and (.key | IN($skip[])))) | not)
| "\(.key): \(.value.result)"' <<< "$NEEDS")
if [ -n "$NOT_PASSED" ]; then
echo "Not passed:"
echo "$NOT_PASSED"
exit 1
fi
echo "All checks passed."
8 changes: 7 additions & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@ permissions:
pages: write
id-token: write

# Two quick pushes must not race each other onto Pages.
concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- run: uv sync --no-dev --extra docs
# --locked: fail when uv.lock is out of date with pyproject.toml.
- run: uv sync --locked --no-dev --extra docs
- run: cp CHANGELOG.md docs/changelog.md
- run: uv run zensical build --clean
- run: uv run python scripts/emit_markdown.py
Expand Down
146 changes: 118 additions & 28 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
name: Publish

# What reaches PyPI is decided here and in the `pypi` environment, which
# setup_repo.py makes deployable from master only. Both triggers run with
# master as their ref, so the checks below are always master's own; a run from
# any other branch is refused the environment before it can publish.
on:
workflow_run:
workflows: ["Release Please"]
types: [completed]
# A manual publish, from master: gh workflow run publish.yml -f tag=<tag>
workflow_dispatch:
inputs:
tag:
Expand All @@ -12,64 +17,149 @@ on:

permissions:
contents: read
id-token: write # Trusted Publisher (PyPI OIDC)

jobs:
check:
name: Check release tag
name: Check release
if: >
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'master')
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
actions: read # the CI run on the released commit
outputs:
tag: ${{ steps.tag.outputs.tag }}
tag: ${{ steps.release.outputs.tag }}
sha: ${{ steps.release.outputs.sha }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.event.workflow_run.head_sha }}
fetch-tags: true
fetch-depth: 0 # master's history and every tag

- name: Resolve tag
id: tag
# The tag names a commit; it is never the ref this run starts from. It has
# to be on master, and the version it carries has to be the one committed
# in __version__.py and in pyproject.toml -- the artifacts are built with
# pyproject.toml's static version, which Release Please bumps together with
# __version__.py, so a tag that disagrees with either is a mistake to stop
# on, not to paper over.
- name: Resolve the release tag
id: release
env:
EVENT: ${{ github.event_name }}
REF: ${{ github.ref }}
INPUT_TAG: ${{ inputs.tag }}
PUSHED_SHA: ${{ github.event.workflow_run.head_sha }}
# Both tag conventions, the component-prefixed one and the bare one,
# so a Release Please config change cannot silently stop publishing.
TAG_PATTERN: '^(mattermind-)?v[0-9][0-9A-Za-z.+-]*$'
TAG_PREFIX: "mattermind-"
VERSION_FILE: "src/mattermind/__version__.py"
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
if [ "$EVENT" = workflow_dispatch ]; then
if [ "$REF" != refs/heads/master ]; then
echo "::error::Publish from master, not $REF: gh workflow run publish.yml --ref master -f tag=<tag>"
exit 1
fi
if [[ ! "$INPUT_TAG" =~ $TAG_PATTERN ]]; then
echo "::error::'$INPUT_TAG' is not a release tag of this project"
exit 1
fi
TAG=$INPUT_TAG
if ! SHA=$(git rev-parse -q --verify "refs/tags/$TAG^{commit}"); then
echo "::error::There is no tag $TAG"
exit 1
fi
else
TAG=$(git tag --points-at HEAD | grep -E '^mattermind-v' | head -1)
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
SHA=$PUSHED_SHA
TAG=$(git tag --points-at "$SHA" | grep -E "$TAG_PATTERN" | head -1 || true)
if [ -z "$TAG" ]; then
echo "No release tag on this commit — skipping publish."
else
echo "Will publish $TAG"
echo "No release tag on $SHA, nothing to publish."
exit 0
fi
fi

if ! git merge-base --is-ancestor "$SHA" origin/master; then
echo "::error::$TAG points at $SHA, which is not on master"
exit 1
fi

VERSION=${TAG#"$TAG_PREFIX"}
VERSION=${VERSION#v}
COMMITTED=$(git show "$SHA:$VERSION_FILE" | sed -n 's/^__version__ = "\([^"]*\)".*/\1/p')
if [ "$COMMITTED" != "$VERSION" ]; then
echo "::error::$TAG is version $VERSION, but $VERSION_FILE at $SHA says '$COMMITTED'"
exit 1
fi
BUILT=$(git show "$SHA:pyproject.toml" | sed -n 's/^version = "\([^"]*\)".*/\1/p')
if [ "$BUILT" != "$VERSION" ]; then
echo "::error::$TAG is version $VERSION, but pyproject.toml at $SHA says '$BUILT'"
exit 1
fi

echo "Will publish $TAG ($SHA)"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "sha=$SHA" >> "$GITHUB_OUTPUT"

# Release Please and CI start from the same push, and Release Please is
# done minutes earlier: without this wait a release is uploaded before CI
# has said anything about the commit being released. A version on PyPI
# cannot be replaced. If CI failed, fix or re-run it, then re-run this.
- name: Wait for CI on the released commit
if: steps.release.outputs.tag != ''
env:
GH_TOKEN: ${{ github.token }}
SHA: ${{ steps.release.outputs.sha }}
run: |
runs="repos/$GITHUB_REPOSITORY/actions/workflows/ci.yml/runs?head_sha=$SHA&event=push&branch=master"
RUN=
for _ in 1 2 3 4 5 6; do
RUN=$(gh api "$runs" --jq '.workflow_runs[0].id // empty' || true)
[ -n "$RUN" ] && break
sleep 20
done
if [ -z "$RUN" ]; then
echo "::error::No CI run for $SHA on master"
exit 1
fi
echo "Waiting for $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$RUN"
until [ "$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN" --jq .status)" = completed ]; do
sleep 20
done
CONCLUSION=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN" --jq .conclusion)
if [ "$CONCLUSION" != success ]; then
echo "::error::CI on $SHA ended '$CONCLUSION'"
exit 1
fi
echo "CI passed on $SHA"

publish:
name: Build and publish to PyPI
needs: check
if: needs.check.outputs.tag != ''
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write # Trusted Publisher (PyPI OIDC)
steps:
# The commit check resolved, not the tag: a tag moved since then changes nothing.
- uses: actions/checkout@v7
with:
ref: ${{ needs.check.outputs.tag }}
ref: ${{ needs.check.outputs.sha }}

- uses: astral-sh/setup-uv@v7

# Release Please already committed this version; the rewrite is a safety
# net so a tag can never publish a stale version.
- name: Set version from release tag
run: |
VERSION="${{ needs.check.outputs.tag }}"
VERSION="${VERSION#mattermind-v}"
sed -i "0,/^version = .*/s//version = \"$VERSION\"/" pyproject.toml
echo "__version__ = \"$VERSION\" # x-release-please-version" > src/mattermind/__version__.py
grep -m1 '^version = ' pyproject.toml
cat src/mattermind/__version__.py

- run: uv build
# --check-url: a run repeated after a failure skips what is already on the
# index instead of failing on it.
# PEP 740 provenance. uv publish uploads attestations but does not make
# them: this signs each distribution with the job's OIDC identity
# (Sigstore) into <file>.publish.attestation, and PyPI rejects an upload
# whose attestation does not match the Trusted Publisher.
- run: uvx --from 'pypi-attestations>=0.0.30' pypi-attestations sign dist/*.whl dist/*.tar.gz
# No file arguments: uv's default, dist/*, takes in the .attestation files
# too; naming the distributions would upload them without their
# attestations. --check-url: a run repeated after a failure skips what is
# already on the index instead of failing on it.
- run: uv publish --check-url https://pypi.org/simple/mattermind/
24 changes: 24 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,31 @@ jobs:
release-please:
runs-on: ubuntu-latest
steps:
# A pull request opened or updated with GITHUB_TOKEN does not get a CI run
# that ever starts, so the release PR could not pass its required check
# until a person closed and reopened it. A token of the org's release App
# starts CI like anyone else's push. Until the App is set up, GITHUB_TOKEN
# is used and the run says so. Setup, once per org:
# https://github.com/bedrock-python/python-library-template#the-release-app
- name: Token from the release App
id: app
if: vars.RELEASE_PLEASE_APP_CLIENT_ID != ''
uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.RELEASE_PLEASE_APP_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write

- name: No release App
if: steps.app.outcome == 'skipped'
run: >-
echo "::warning::RELEASE_PLEASE_APP_CLIENT_ID is not set, so the release PR
is opened with GITHUB_TOKEN and CI will not run on it: close and reopen it
right before merging."

- uses: googleapis/release-please-action@v5
with:
token: ${{ steps.app.outputs.token || github.token }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
Loading