Remove GitHub workflow configurations
This commit is contained in:
parent
52cd0c1bb4
commit
6d7524bbe3
4 changed files with 0 additions and 912 deletions
75
.github/workflows/codeql-analysis.yml
vendored
75
.github/workflows/codeql-analysis.yml
vendored
|
|
@ -1,75 +0,0 @@
|
|||
# CodeQL static analysis. Results land in the Security tab under Code scanning.
|
||||
#
|
||||
# This was the stock 2020 starter template until 2026-08-12. It ran green for
|
||||
# years, then failed 44 consecutive runs between 2026-08-01 and 2026-08-12,
|
||||
# every push and PR, with "Resource not accessible by integration" on the
|
||||
# code-scanning status endpoint.
|
||||
#
|
||||
# The cause was not the workflow changing, which is why it looked mysterious:
|
||||
# this repository's default workflow permissions are set to `read` (see Settings
|
||||
# -> Actions -> Workflow permissions). That is the right default, but the stock
|
||||
# template declared no `permissions:` block at all, so the job's token could not
|
||||
# write the SARIF it exists to produce. The block below is the fix.
|
||||
#
|
||||
# The v1 -> v3 action bump in the same change was overdue on its own (v1 was
|
||||
# retired in January 2023) but was NOT what broke this. Do not read the bump as
|
||||
# the repair; the `permissions:` block is the repair.
|
||||
#
|
||||
# The failure mode is worth remembering: the Security tab went on showing the
|
||||
# results of the last upload that worked, dated 2026-07-31, with nothing
|
||||
# marking them stale. A scanner failing loudly in Actions still reads as a
|
||||
# scanner passing quietly if you only ever look at the Security tab.
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '41 18 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Declaring this block at all drops every permission not listed to `none`,
|
||||
# so these two are the whole set the job gets. `security-events: write` is
|
||||
# the one that was missing and the only reason the job exists: it is what
|
||||
# lets `analyze` upload the SARIF. `contents: read` is for the checkout.
|
||||
# The upstream template also lists `packages: read` and `actions: read`,
|
||||
# both needed only by private repositories. This one is public.
|
||||
permissions:
|
||||
security-events: write
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Python only. The SvelteKit frontend is not scanned; adding
|
||||
# `javascript-typescript` here is a real coverage gain and a real new
|
||||
# alert backlog, so it is a decision rather than a fix.
|
||||
language: ['python']
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# No Autobuild step. The template shipped one, but it only does anything
|
||||
# for compiled languages (C/C++, C#, Java, Go). Python needs no build, so
|
||||
# the step was config that could never do anything.
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
# Keeps each matrix language's results a separate analysis. Without
|
||||
# it a second language added above would overwrite this one's alerts
|
||||
# rather than sit alongside them.
|
||||
category: "/language:${{ matrix.language }}"
|
||||
318
.github/workflows/publish.yml
vendored
318
.github/workflows/publish.yml
vendored
|
|
@ -1,318 +0,0 @@
|
|||
name: Publish to PyPI
|
||||
|
||||
# Publishing is the one irreversible thing this repo does. PyPI refuses a second
|
||||
# upload of a version that already exists, so a wrong or broken artifact cannot
|
||||
# be replaced, only yanked and superseded. Every gate below exists because of
|
||||
# that: verify the built distribution before it leaves, not after.
|
||||
#
|
||||
# `django-crm` on PyPI is an existing project (0.2.0 in 2019 through 0.9.0 in
|
||||
# 2021, then dormant). This resumes it rather than claiming a new name. Note
|
||||
# that PyPI stopped at 0.9.0 while git carried on to 1.0, 1.1 and 1.2, so those
|
||||
# three tags have no release behind them.
|
||||
#
|
||||
# Three ways in. The first is the normal one; the other two exist because they
|
||||
# were already possible and going silent on them would be worse:
|
||||
#
|
||||
# 1. workflow_dispatch on a TAG ref, from tag-release.yml, against the tag it
|
||||
# just derived from backend/pyproject.toml. Nobody types a version here.
|
||||
# This path exists because a tag pushed by tag-release.yml's GITHUB_TOKEN
|
||||
# does NOT fire the `push` trigger below: GitHub suppresses workflow runs
|
||||
# from events its own token caused, so workflows cannot loop.
|
||||
# 2. push of a tag a HUMAN created. A person's token is not suppressed, so
|
||||
# this fires normally. Prefer path 1; this one can still disagree with
|
||||
# backend/pyproject.toml, which is what the version guard is for.
|
||||
# 3. workflow_dispatch on a BRANCH ref, which rehearses a release end to end
|
||||
# without uploading anything: `publish` and `frontend-image` are gated on
|
||||
# a tag ref, so both skip. Read the version-guard step's output, which
|
||||
# says which checks a rehearsal did not run.
|
||||
#
|
||||
# Exactly one of 1 and 2 fires per release, so they do not double-publish.
|
||||
#
|
||||
# See docs/contributing/releasing.md.
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
# Both spellings on purpose. Every tag in this repo's history before
|
||||
# v1.3.0 is unprefixed (1.2, 1.1, 0.9.0), so matching only 'v*' would
|
||||
# mean someone tagging 1.4.0 out of habit gets silence instead of a
|
||||
# release, and silence is the worst possible failure for a publish
|
||||
# trigger. The version check below strips a leading v when there is one.
|
||||
# Tags that tag-release.yml creates are always v-prefixed.
|
||||
- 'v[0-9]*'
|
||||
- '[0-9]*'
|
||||
workflow_dispatch:
|
||||
|
||||
# Read-only by default. Only `publish` gets id-token: write and only
|
||||
# `frontend-image` gets packages: write, so no other job holds a token that can
|
||||
# publish anything. Without this line every job inherits the repository default,
|
||||
# which on many repos is write-all.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: publish-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# tests.yml is branch-scoped (`push: branches: ['**']`) AND path-filtered, so
|
||||
# a tag push runs no tests of its own and a commit that touched only, say,
|
||||
# README.md (the PyPI long description) or this file ran none either. A
|
||||
# release is exactly the moment nobody is watching CI, so the suite runs here
|
||||
# against the commit actually being shipped.
|
||||
#
|
||||
# This is the SQLite run only. The PostgreSQL corner in tests.yml (a
|
||||
# non-superuser role, the RLS isolation pass) is not repeated here: it is
|
||||
# ~40 lines of service container and role setup that would drift from the
|
||||
# copy that matters. So this gate proves the suite passes, not that RLS
|
||||
# isolation does. Merge to master with that job green before releasing.
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: 'backend/uv.lock'
|
||||
|
||||
- name: Install system dependencies (WeasyPrint)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libpango-1.0-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
libgdk-pixbuf-2.0-0 \
|
||||
libffi-dev \
|
||||
shared-mime-info
|
||||
|
||||
- name: Install Python dependencies
|
||||
working-directory: backend
|
||||
run: uv sync --frozen
|
||||
|
||||
# The suite runs with --no-migrations, so nothing else here would catch a
|
||||
# model change shipped without its migration. That reaches a user as a
|
||||
# broken `migrate` on upgrade, which is worse than a failed release.
|
||||
- name: Check for missing migrations
|
||||
working-directory: backend
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: crm.test_settings
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ADMIN_EMAIL: admin@test.com
|
||||
run: uv run python manage.py makemigrations --check --dry-run
|
||||
|
||||
- name: Run the test suite
|
||||
working-directory: backend
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: crm.test_settings
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ADMIN_EMAIL: admin@test.com
|
||||
run: uv run pytest -m "not postgres_only" --no-cov --tb=short
|
||||
|
||||
build:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v10.0.1
|
||||
with:
|
||||
# Deliberately off here, and on in every other job. This is the job
|
||||
# whose output is uploaded to PyPI under the project's name, and the
|
||||
# GitHub Actions cache is writable from less-trusted contexts: a
|
||||
# `pull_request` run from a fork can poison an entry that a later run
|
||||
# on master restores. A cache is therefore an input to the artifact,
|
||||
# and this artifact is the one thing here that cannot be recalled.
|
||||
#
|
||||
# setup-uv v10 made exactly this its headline change, and it is still
|
||||
# not enough on its own. Under `enable-cache: auto` v10 skips the
|
||||
# cache for `pull_request_target`, `workflow_run` and `release`. This
|
||||
# workflow's normal entry point is `workflow_dispatch` on a tag,
|
||||
# which is in none of those, so `auto` would happily cache here. An
|
||||
# explicit `true` is worse than useless: setup-uv reads it before any
|
||||
# event test, so it opts out of the protection entirely. Only an
|
||||
# explicit `false` is actually off.
|
||||
enable-cache: false
|
||||
|
||||
# The tag is what people will `pip install ==`, and the version inside the
|
||||
# wheel is what they actually get. Nothing else compares them, and a
|
||||
# mismatch is only discoverable after the upload it cannot be undone from.
|
||||
#
|
||||
# Skipped on a non-tag ref, which is the workflow_dispatch rehearsal
|
||||
# described above. That skip is announced rather than silent: a skipped
|
||||
# guard and a passing one look identical in the Actions UI, and the whole
|
||||
# point of a rehearsal is knowing which checks it actually ran.
|
||||
- name: Tag matches the packaged version
|
||||
working-directory: backend
|
||||
run: |
|
||||
packaged="$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")"
|
||||
case "$GITHUB_REF" in
|
||||
refs/tags/*) ;;
|
||||
*)
|
||||
echo "::warning::Not a tag ref ($GITHUB_REF): skipping the tag/version check. This is a rehearsal, and it will NOT publish."
|
||||
echo "would package version $packaged"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
tag="${GITHUB_REF_NAME#v}"
|
||||
if [ "$tag" != "$packaged" ]; then
|
||||
echo "::error::tag $GITHUB_REF_NAME does not match pyproject.toml version $packaged"
|
||||
exit 1
|
||||
fi
|
||||
echo "publishing version $packaged"
|
||||
|
||||
- name: Build the wheel and sdist
|
||||
working-directory: backend
|
||||
run: uv build --out-dir "$RUNNER_TEMP/dist"
|
||||
|
||||
# README.md is the long description, so a construct PyPI will not render
|
||||
# fails here rather than landing as a broken project page on a version
|
||||
# that cannot be replaced. The publish action verifies metadata too, but
|
||||
# only on the tag path: this is what makes a workflow_dispatch dry run
|
||||
# tell you anything about the description.
|
||||
#
|
||||
# `--strict` because twine's default turns a rendering WARNING into a
|
||||
# green tick, and PyPI publishes a description it cannot render as an
|
||||
# empty project page rather than as an error. The warning is the only
|
||||
# notice you get, and only if someone reads the log. `readme-renderer[md]`
|
||||
# is what teaches twine to check Markdown at all; without it a Markdown
|
||||
# long description is waved through unexamined.
|
||||
- name: Check the distribution metadata
|
||||
run: uvx --with "readme-renderer[md]" twine check --strict "$RUNNER_TEMP"/dist/*
|
||||
|
||||
# `check` walks the URLconf into the invoices views, which import
|
||||
# WeasyPrint at module scope.
|
||||
- name: Install system dependencies (WeasyPrint)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libpango-1.0-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
libgdk-pixbuf-2.0-0 \
|
||||
libffi-dev \
|
||||
shared-mime-info
|
||||
|
||||
# The same gate as the package-build job in tests.yml, repeated here
|
||||
# because that job runs on the branch and this runs on the tag. Run from
|
||||
# $RUNNER_TEMP: inside the source tree every app imports from the working
|
||||
# directory, which is what let two installed apps ship missing from the
|
||||
# wheel with a green suite.
|
||||
- name: Install the built wheel into a clean environment and start it
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: crm.settings
|
||||
SECRET_KEY: startup-check-only-not-a-deployment-secret
|
||||
run: |
|
||||
cd "$RUNNER_TEMP"
|
||||
uv venv --python 3.12 pkgenv
|
||||
uv pip install --python pkgenv/bin/python dist/*.whl
|
||||
mkdir -p neutral && cd neutral
|
||||
"$RUNNER_TEMP/pkgenv/bin/django-admin" check
|
||||
"$RUNNER_TEMP/pkgenv/bin/python" -c "import django; django.setup(); from django.template.loader import get_template; [get_template(n) for n in ('magic_link_email.html', 'magic_link_code_email.html', 'root_email_template_new.html', 'healthz.html', 'assigned_to/leads_assigned.html', 'opportunity/goal_milestone.html', 'csat/survey_email.html')]; print('templates resolve from the installed package')"
|
||||
|
||||
- name: Upload the distributions
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ runner.temp }}/dist/
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Trusted Publishing (OIDC). No API token is stored in this repo, so there
|
||||
# is no long-lived PyPI credential to leak or rotate, and publish rights are
|
||||
# bound to this repository, this workflow file and this environment.
|
||||
#
|
||||
# Configured once on pypi.org under the project's Publishing settings, with
|
||||
# GitHub's own casing for the first two, which is what lands in the OIDC
|
||||
# token claims:
|
||||
# owner: Django-CRM <- the org the repo was TRANSFERRED to. The
|
||||
# PyPI project page still shows
|
||||
# MicroPyramid from the 2021 release, and
|
||||
# MicroPyramid/Django-CRM now redirects
|
||||
# here. The publisher must name where the
|
||||
# code lives, not where it used to.
|
||||
# repository: Django-CRM
|
||||
# workflow: publish.yml <- this file, matched by filename
|
||||
# environment: pypi <- must equal `environment.name` below
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/project/django-crm/
|
||||
|
||||
permissions:
|
||||
# The only permission the OIDC exchange needs. Deliberately no
|
||||
# `contents: write`: publishing does not modify the repository.
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Download the distributions
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
# Pinned to a commit SHA rather than the `v1` tag used elsewhere in this
|
||||
# repo. A moving tag means whatever the upstream owner repoints it to runs
|
||||
# inside the job holding the OIDC token that can publish as us. tests.yml
|
||||
# can afford tags; this job cannot.
|
||||
# dc37677b2e1c63e2034f94d8a5b11f265b73ba33 is pypa/gh-action-pypi-publish
|
||||
# v1.14.2. Bump both together, never the comment alone.
|
||||
- name: Publish
|
||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
|
||||
|
||||
# The other half of the release. PyPI carries the API; this carries the UI,
|
||||
# which is a Node server (adapter-node, 81 server load functions and 64 form
|
||||
# actions) and cannot be a Python package. Built from the SAME tag as the
|
||||
# wheel above so "enterprise runs community 1.4.2" names one commit rather
|
||||
# than an API version and whatever the UI happened to be.
|
||||
frontend-image:
|
||||
# Gated on `publish`, not just `build`. The two artifacts are one release
|
||||
# under one tag, so an image tagged 1.3.0 must never exist without the
|
||||
# matching PyPI release. Gating on `build` alone was not enough and said so
|
||||
# while being wrong: v1.3.0 built fine, failed the PyPI OIDC exchange, and
|
||||
# still pushed ghcr.io/django-crm/bottlecrm-frontend:1.3.0 and :latest with
|
||||
# nothing on PyPI to match. `latest` is the damaging half, since it moves.
|
||||
# Both jobs are already tag-only, so depending on a job that is skipped on
|
||||
# workflow_dispatch skips this one too, which is what a dry run wants.
|
||||
needs: [build, publish]
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# Plain docker CLI rather than docker/login-action + build-push-action.
|
||||
# This job can push an image under the org's namespace, so the fewer
|
||||
# third-party actions holding that token the better. The CLI is already
|
||||
# on the runner and the whole recipe is four lines.
|
||||
- name: Build and push
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
version="${GITHUB_REF_NAME#v}"
|
||||
owner="$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')"
|
||||
image="${REGISTRY}/${owner}/bottlecrm-frontend"
|
||||
|
||||
echo "${GHCR_TOKEN}" | docker login "${REGISTRY}" -u "${GITHUB_ACTOR}" --password-stdin
|
||||
|
||||
# The default (last) stage is the production server. Naming it is not
|
||||
# optional: `target: dev` in docker-compose.yml means the stage order
|
||||
# is load-bearing, and a silent change to it would ship a Vite dev
|
||||
# server to everyone who pulls this tag.
|
||||
docker build --target runtime \
|
||||
-t "${image}:${version}" \
|
||||
-t "${image}:latest" \
|
||||
frontend/
|
||||
|
||||
docker push "${image}:${version}"
|
||||
docker push "${image}:latest"
|
||||
142
.github/workflows/tag-release.yml
vendored
142
.github/workflows/tag-release.yml
vendored
|
|
@ -1,142 +0,0 @@
|
|||
name: Tag release
|
||||
|
||||
# The tag is DERIVED from backend/pyproject.toml. Nobody types it.
|
||||
#
|
||||
# publish.yml already refuses a tag whose version disagrees with the packaged
|
||||
# one, which is the right guard and does not go far enough: it turns the
|
||||
# mistake into a failed release rather than into no mistake. Recovering still
|
||||
# means deleting a tag, and a leftover tag is itself a trap (see the "already
|
||||
# exists" branch below). backend/pyproject.toml is the only place this version
|
||||
# is written, so a hand-typed tag was the single remaining thing that could
|
||||
# disagree with it. Now nothing types it.
|
||||
#
|
||||
# The whole release process:
|
||||
#
|
||||
# bump `version` in backend/pyproject.toml, open a PR, merge it
|
||||
#
|
||||
# That is the end of the list. This workflow creates tag v<version>, publishes
|
||||
# a GitHub Release with generated notes, and starts publish.yml, which builds
|
||||
# the wheel, uploads it to PyPI and pushes the frontend image to GHCR.
|
||||
#
|
||||
# See docs/contributing/releasing.md.
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
# pyproject.toml changes for plenty of reasons that are not a version bump:
|
||||
# a dependency, a classifier, a ruff setting. This only narrows what wakes
|
||||
# the workflow up. The real decision is "does that tag already exist",
|
||||
# below, which is what makes re-running this harmless.
|
||||
paths: ['backend/pyproject.toml']
|
||||
# Lets a version already on master be released without touching the file,
|
||||
# including the first run, when the current version predates this workflow
|
||||
# and no push will ever mention it.
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write # create the tag and the GitHub Release
|
||||
actions: write # dispatch publish.yml
|
||||
|
||||
concurrency:
|
||||
group: tag-release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
name: Tag and release if the version changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
# Existing tags are how this decides whether a version was already
|
||||
# released, and a shallow clone fetches none of them. It would
|
||||
# re-release every version, every time.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v10.0.1
|
||||
|
||||
- name: Read the version and decide whether it needs releasing
|
||||
id: v
|
||||
run: |
|
||||
version=$(python3 -c "import tomllib, pathlib; print(tomllib.loads(pathlib.Path('backend/pyproject.toml').read_text())['project']['version'])")
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# BOTH spellings, and that is not defensive padding. Every tag in
|
||||
# this repo before v1.3.0 is unprefixed (0.9.0, 1.0, 1.1, 1.2) and
|
||||
# publish.yml still triggers on both, so a version released under a
|
||||
# bare tag must not be re-released under a v-prefixed one. New tags
|
||||
# are always v-prefixed; this only reads the old ones.
|
||||
if git rev-parse -q --verify "refs/tags/v${version}" >/dev/null ||
|
||||
git rev-parse -q --verify "refs/tags/${version}" >/dev/null; then
|
||||
echo "release=false" >> "$GITHUB_OUTPUT"
|
||||
# Say so where a human will actually see it. A no-op run is green
|
||||
# and its logs are collapsed, so "did nothing" and "released it"
|
||||
# look identical from the Actions list. This matters most in the
|
||||
# case that bites: a LEFTOVER tag from an abandoned release means
|
||||
# bumping to that same version later silently publishes nothing.
|
||||
echo "::warning::No release: a tag for ${version} already exists. If you meant to release ${version}, delete that tag and its GitHub Release first."
|
||||
{
|
||||
echo "### No release"
|
||||
echo "\`backend/pyproject.toml\` says **${version}**, and a tag for it already exists."
|
||||
echo "Nothing to do. If this was meant to be a release, delete that tag and its GitHub Release, then re-run this workflow."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "release=true" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "### Releasing ${version}"
|
||||
echo "Creating tag \`v${version}\` on \`${GITHUB_SHA}\` and starting \`publish.yml\`."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
# PEP 440 decides this, not a substring search: 1.4.0rc1 is a
|
||||
# pre-release and 1.4.0.post1 is not. GitHub labels the Release from
|
||||
# the answer, and getting it wrong shows a release candidate to
|
||||
# everyone watching the repo as though it were final.
|
||||
# --no-project because this runs from the repo root, which has no
|
||||
# pyproject.toml of its own (the package lives in backend/). uv would
|
||||
# cope today by finding no project at all, but it searches UPWARD, so
|
||||
# adding one at the root later would silently make this step install
|
||||
# and build the whole application to read one version string.
|
||||
prerelease=$(uv run --no-project --with packaging python -c "from packaging.version import Version; print(str(Version('${version}').is_prerelease).lower())")
|
||||
echo "prerelease=${prerelease}" >> "$GITHUB_OUTPUT"
|
||||
echo "django-crm ${version} (prerelease: ${prerelease})"
|
||||
|
||||
- name: Create the tag and the GitHub Release
|
||||
if: steps.v.outputs.release == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ steps.v.outputs.version }}
|
||||
PRERELEASE: ${{ steps.v.outputs.prerelease }}
|
||||
run: |
|
||||
flags=""
|
||||
if [ "$PRERELEASE" = "true" ]; then
|
||||
flags="--prerelease"
|
||||
fi
|
||||
# --target pins the tag to the exact commit that carried the bump,
|
||||
# not to whatever master has drifted to since this run started.
|
||||
gh release create "v${VERSION}" \
|
||||
--target "$GITHUB_SHA" \
|
||||
--title "v${VERSION}" \
|
||||
--generate-notes \
|
||||
$flags
|
||||
|
||||
- name: Start the publish workflow
|
||||
if: steps.v.outputs.release == 'true'
|
||||
# Creating that tag and Release with GITHUB_TOKEN fires NEITHER of
|
||||
# publish.yml's own triggers. GitHub suppresses workflow runs caused by
|
||||
# its own token so workflows cannot loop, and that covers both the tag
|
||||
# push and the release event. Relying on either would fail silently:
|
||||
# the release appears, nothing publishes, and no run exists anywhere to
|
||||
# explain why. workflow_dispatch is one of the two documented
|
||||
# exceptions, which is the only reason this chain works.
|
||||
#
|
||||
# This is also why publish.yml keeps its `push: tags` trigger: that one
|
||||
# is for a tag a human pushes by hand. Exactly one of the two fires per
|
||||
# release, so they do not double-publish.
|
||||
#
|
||||
# --ref puts github.ref on refs/tags/v*, which is what publish.yml's
|
||||
# version guard and its publish/frontend-image jobs all key off.
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ steps.v.outputs.version }}
|
||||
run: gh workflow run publish.yml --ref "v${VERSION}"
|
||||
377
.github/workflows/tests.yml
vendored
377
.github/workflows/tests.yml
vendored
|
|
@ -1,377 +0,0 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
# Every branch, not just master. `pull_request` below already gates the
|
||||
# merge, so the branch filter here only decided whether work-in-progress
|
||||
# got feedback before a PR existed, and the answer was no. `'**'` matches
|
||||
# branch names containing slashes; naming `branches` at all is also what
|
||||
# keeps tag pushes from triggering a second run of the same commit.
|
||||
branches: ['**']
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'frontend/**'
|
||||
- 'docs/**'
|
||||
- 'mkdocs.yml'
|
||||
- '.readthedocs.yaml'
|
||||
- '.coveragerc'
|
||||
- '.github/workflows/tests.yml'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'frontend/**'
|
||||
- 'docs/**'
|
||||
- 'mkdocs.yml'
|
||||
- '.readthedocs.yaml'
|
||||
- '.coveragerc'
|
||||
- '.github/workflows/tests.yml'
|
||||
|
||||
# Running on every branch push means a branch with an open PR would otherwise
|
||||
# bill two identical runs per push, and a rapid series of pushes would queue
|
||||
# runs for commits nobody is waiting on any more. One group per ref, newest
|
||||
# wins.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
backend-tests:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: 'backend/uv.lock'
|
||||
|
||||
- name: Install system dependencies (WeasyPrint)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libpango-1.0-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
libgdk-pixbuf-2.0-0 \
|
||||
libffi-dev \
|
||||
shared-mime-info
|
||||
|
||||
- name: Install Python dependencies
|
||||
working-directory: backend
|
||||
run: uv sync --frozen
|
||||
|
||||
# Running the migrations cannot notice a model change that has no
|
||||
# migration yet, so this checks for one explicitly.
|
||||
- name: Check for missing migrations
|
||||
working-directory: backend
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: crm.test_settings
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ADMIN_EMAIL: admin@test.com
|
||||
run: uv run python manage.py makemigrations --check --dry-run
|
||||
|
||||
- name: Run tests with coverage
|
||||
working-directory: backend
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: crm.test_settings
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ADMIN_EMAIL: admin@test.com
|
||||
run: uv run pytest -m "not postgres_only" --tb=short -v
|
||||
|
||||
- name: Generate coverage badge
|
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||
working-directory: backend
|
||||
run: uvx --from "genbadge[coverage]" genbadge coverage -i coverage.xml -o ../coverage-badge.svg
|
||||
|
||||
- name: Commit coverage badge
|
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add coverage-badge.svg || true
|
||||
git diff --staged --quiet || git commit -m "Update coverage badge [skip ci]"
|
||||
git push || true
|
||||
|
||||
- name: Upload coverage report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: coverage-report
|
||||
path: backend/htmlcov/
|
||||
retention-days: 14
|
||||
|
||||
# The job above runs on SQLite, which is fast and blind to a whole class of
|
||||
# defect: declared varchar lengths, RLS policies, and every test carrying the
|
||||
# `postgres_only` marker (which the SQLite job deselects, so they had never
|
||||
# run in CI at all). Two shipped bugs were invisible to a green suite for
|
||||
# exactly this reason; see crm/test_settings_postgres.py.
|
||||
backend-tests-postgres:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: 'backend/uv.lock'
|
||||
|
||||
- name: Install system dependencies (WeasyPrint)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libpango-1.0-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
libgdk-pixbuf-2.0-0 \
|
||||
libffi-dev \
|
||||
shared-mime-info
|
||||
|
||||
# A superuser bypasses RLS, and the policies use FORCE ROW LEVEL
|
||||
# SECURITY so even the owning role is subject to them. Running the
|
||||
# RLS tests as `postgres` makes every isolation assertion vacuously
|
||||
# true, the same trap RLS_SETUP.md warns about for local development.
|
||||
# CREATEDB is what lets pytest-django build the test database.
|
||||
- name: Create a non-superuser role for the RLS tests
|
||||
env:
|
||||
PGPASSWORD: postgres
|
||||
run: |
|
||||
psql -h localhost -U postgres -c \
|
||||
"CREATE ROLE crm_ci WITH LOGIN CREATEDB PASSWORD 'crm_ci' NOSUPERUSER NOBYPASSRLS;"
|
||||
|
||||
- name: Install Python dependencies
|
||||
working-directory: backend
|
||||
run: uv sync --frozen
|
||||
|
||||
# Run 1: the whole suite, for what SQLite cannot see. Declared varchar
|
||||
# lengths, integrity constraints, real type coercion. It runs as the
|
||||
# superuser, so RLS is inert here; run 2 is where isolation is tested.
|
||||
# The reason for the split is that the shared fixtures insert rows with
|
||||
# no `app.current_org` set, which a policy-enforcing role refuses. That
|
||||
# is a real gap in the fixtures rather than a CI setting, and closing it
|
||||
# is a change to almost every test file, so it is deliberately not
|
||||
# bundled into this one. Until then the split says plainly which run
|
||||
# covers which.
|
||||
- name: Run the full suite against PostgreSQL
|
||||
working-directory: backend
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: crm.test_settings_postgres
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ADMIN_EMAIL: admin@test.com
|
||||
DBNAME: crm_db
|
||||
DBUSER: postgres
|
||||
DBPASSWORD: postgres
|
||||
DBHOST: localhost
|
||||
DBPORT: '5432'
|
||||
# `postgres_only` is deselected here for the same reason it runs in
|
||||
# the next step instead: those tests assert that RLS hides rows, and
|
||||
# this run's superuser role is not subject to any policy, so they fail
|
||||
# here for a reason that is about the role rather than the code.
|
||||
# No coverage: the SQLite job produces the report and the badge.
|
||||
run: uv run pytest --no-cov --tb=short -q -m "not postgres_only"
|
||||
|
||||
# Run 2: the `postgres_only` tests, as a role that RLS actually binds.
|
||||
# The SQLite job deselects these, so before this step they had never
|
||||
# executed in CI at all. They also have to run as `crm_ci` to mean
|
||||
# anything: as a superuser they pass whether or not a single policy
|
||||
# exists.
|
||||
- name: Run the RLS tests as a policy-bound role
|
||||
working-directory: backend
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: crm.test_settings_postgres
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ADMIN_EMAIL: admin@test.com
|
||||
DBNAME: crm_db
|
||||
DBUSER: crm_ci
|
||||
DBPASSWORD: crm_ci
|
||||
DBHOST: localhost
|
||||
DBPORT: '5432'
|
||||
run: uv run pytest --no-cov --tb=short -q -m postgres_only
|
||||
|
||||
# Bare `ruff check .`, so the gate is whatever `backend/ruff.toml` selects
|
||||
# (E, F, I with E501 ignored) rather than a CLI override that can silently
|
||||
# drift from the file every editor and pre-commit hook reads.
|
||||
#
|
||||
# F is the half that catches defects rather than style: F811 is how
|
||||
# `ActivitySerializer` came to be declared twice, with the second silently
|
||||
# shadowing the first for both of its consumers, and F401 found three
|
||||
# imports left behind by the timezone sweep plus two `# noqa` comments
|
||||
# that suppressed nothing.
|
||||
#
|
||||
# `uv run`, not `uvx`: ruff is a locked dev dependency, so CI and a
|
||||
# developer's machine check against the same version and a ruff release
|
||||
# cannot turn this red overnight.
|
||||
- name: Lint
|
||||
working-directory: backend
|
||||
run: uv run ruff check .
|
||||
|
||||
# `ruff format` rather than black: it is a reimplementation of black with
|
||||
# the same output, `ruff.toml` already carried the `[format]` section, and
|
||||
# one tool cannot disagree with itself the way black and ruff format can
|
||||
# at the edges. `ruff check` above covers isort's job through the I rules.
|
||||
# The tree was formatted in full when this step was added, so the gate
|
||||
# starts green rather than as a warning nobody acts on.
|
||||
- name: Format check
|
||||
working-directory: backend
|
||||
run: uv run ruff format --check .
|
||||
|
||||
# Nothing above this job exercises the built distribution. Development uses an
|
||||
# editable install, which hatchling implements as a single .pth file adding all
|
||||
# of backend/ to sys.path, so every app and template is reachable whether or not
|
||||
# `[tool.hatch.build.targets.wheel]` in pyproject.toml ever lists it. That is how
|
||||
# `business_hours` and `macros` came to be installed apps that no wheel shipped:
|
||||
# a green suite, and `pip install django-crm` dead on ModuleNotFoundError.
|
||||
package-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: 'backend/uv.lock'
|
||||
|
||||
# `check` walks the URLconf, which reaches the invoices views, which import
|
||||
# WeasyPrint at module scope. Without these it fails on a missing .so and
|
||||
# says nothing about packaging.
|
||||
- name: Install system dependencies (WeasyPrint)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libpango-1.0-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
libgdk-pixbuf-2.0-0 \
|
||||
libffi-dev \
|
||||
shared-mime-info
|
||||
|
||||
- name: Build the wheel
|
||||
working-directory: backend
|
||||
run: uv build --wheel --out-dir "$RUNNER_TEMP/dist"
|
||||
|
||||
# Deliberately run from $RUNNER_TEMP rather than backend/. From inside the
|
||||
# source tree `crm` and every app resolve from the working directory, which
|
||||
# reproduces the exact blindness this job exists to remove. The venv gets
|
||||
# the wheel and its declared dependencies, nothing else.
|
||||
- name: Install the wheel into a clean environment
|
||||
run: |
|
||||
cd "$RUNNER_TEMP"
|
||||
uv venv --python 3.12 pkgenv
|
||||
uv pip install --python pkgenv/bin/python dist/*.whl
|
||||
|
||||
# Loads every app in INSTALLED_APPS and every view behind the URLconf, so a
|
||||
# package missing from the wheel fails here. Warnings do not fail `check`,
|
||||
# so keep this output empty: a standing warning is where the next real one
|
||||
# hides.
|
||||
- name: Start the installed package
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: crm.settings
|
||||
SECRET_KEY: startup-check-only-not-a-deployment-secret
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/neutral"
|
||||
cd "$RUNNER_TEMP/neutral"
|
||||
"$RUNNER_TEMP/pkgenv/bin/django-admin" check
|
||||
|
||||
# `check` does not open a template, so it is blind to the second half of
|
||||
# the same defect: these render the magic-link login emails, and they only
|
||||
# ship because they sit in an app's templates/ directory. A project-level
|
||||
# templates/ dir resolves to site-packages once installed.
|
||||
- name: Resolve packaged templates
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: crm.settings
|
||||
SECRET_KEY: startup-check-only-not-a-deployment-secret
|
||||
run: |
|
||||
cd "$RUNNER_TEMP/neutral"
|
||||
"$RUNNER_TEMP/pkgenv/bin/python" -c "import django; django.setup(); from django.template.loader import get_template; [get_template(n) for n in ('magic_link_email.html', 'magic_link_code_email.html', 'root_email_template_new.html', 'healthz.html', 'assigned_to/leads_assigned.html', 'opportunity/goal_milestone.html', 'csat/survey_email.html')]; print('templates resolve from the installed package')"
|
||||
|
||||
frontend-checks:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Prettier format check
|
||||
working-directory: frontend
|
||||
run: pnpm prettier --check . || echo "::warning::Prettier found formatting issues in $(pnpm prettier --check . 2>&1 | grep -c '^\[warn\]') files. Run 'pnpm format' to fix."
|
||||
|
||||
- name: ESLint
|
||||
working-directory: frontend
|
||||
run: pnpm eslint .
|
||||
|
||||
- name: Type check (svelte-check)
|
||||
working-directory: frontend
|
||||
run: pnpm check
|
||||
|
||||
- name: Run tests
|
||||
working-directory: frontend
|
||||
run: pnpm test
|
||||
|
||||
- name: Build
|
||||
working-directory: frontend
|
||||
run: pnpm build
|
||||
|
||||
docs-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: 'backend/uv.lock'
|
||||
|
||||
- name: Install docs dependencies
|
||||
working-directory: backend
|
||||
run: uv sync --frozen --group docs
|
||||
|
||||
# --strict fails on broken internal links and nav entries pointing at
|
||||
# files that do not exist.
|
||||
- name: Build docs
|
||||
working-directory: backend
|
||||
run: uv run mkdocs build --strict -f ../mkdocs.yml -d site
|
||||
|
||||
# These two tests skip when mkdocs is absent, so the backend-tests job
|
||||
# (which installs no docs group) cannot run them. Without this step the
|
||||
# "no internal artifacts published" guard would silently never execute
|
||||
# in CI. A skip reads as a pass.
|
||||
- name: Check docs build guards
|
||||
working-directory: backend
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: crm.test_settings
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ADMIN_EMAIL: admin@test.com
|
||||
run: uv run pytest common/tests/test_docs_build_excludes_internal.py --no-cov -v
|
||||
Loading…
Reference in a new issue