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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ body:
id: fcc-version
attributes:
label: FCC version
description: Run `fcc-server --version` and include one version in `number.number.number` format. If FCC did not install, enter `None`.
description: Run `fcc-server --version` and include one version exactly as printed, including any development suffix. If FCC did not install, enter `None`.
placeholder: "The version is 1.22.333, or None"
validations:
required: true
Expand Down
14 changes: 12 additions & 2 deletions .github/actions/ci-environment/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ inputs:
default: "false"

outputs:
python-version:
description: Exact Python version required by the project
value: ${{ steps.identity.outputs.python-version }}
python-path:
description: Managed Python cache directory
value: ${{ steps.identity.outputs.python-path }}
Expand All @@ -44,6 +47,13 @@ outputs:
runs:
using: composite
steps:
- name: Fetch canonical release tags
shell: bash
env:
CI_SERVER: ${{ github.server_url }}
CI_REPOSITORY: ${{ github.repository }}
run: git fetch --prune --no-tags "$CI_SERVER/$CI_REPOSITORY.git" '+refs/tags/*:refs/tags/*'

- name: Prepare cache identity
id: identity
shell: bash
Expand All @@ -55,8 +65,8 @@ runs:
CI_UV: ${{ inputs.uv-version }}
run: |
set -euo pipefail
version="$(tr -d '[:space:]' < .python-version)"
test -n "$version"
version="$(python -c 'import pathlib, tomllib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))["project"]["requires-python"].removeprefix("=="))')"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo 'Expected an exact requires-python pin in pyproject.toml' >&2; exit 1; }
cache_root="${CI_TEMP//\\//}"
identity="$CI_RUNNER-$CI_OS-$CI_ARCH-py-$version-uv-$CI_UV"
{
Expand Down
1 change: 1 addition & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ version: 2
updates:
- package-ecosystem: "uv"
directory: "/"
versioning-strategy: increase
schedule:
interval: "weekly"
groups:
Expand Down
60 changes: 16 additions & 44 deletions .github/workflows/post-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ env:

on:
workflow_dispatch:
inputs:
release_commit:
description: Full main commit SHA to retry (defaults to this workflow commit)
required: false
type: string
push:
branches: [main]

Expand Down Expand Up @@ -37,7 +42,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ github.sha }}
fetch-depth: 1
fetch-depth: 0

- name: Set up CI environment
id: environment
Expand Down Expand Up @@ -81,62 +86,29 @@ jobs:
timeout-minutes: 15
environment: pypi
permissions:
contents: read
contents: write
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ github.sha }}
fetch-depth: 2

- name: Fetch previous revision
env:
BEFORE_SHA: ${{ github.event.before }}
run: |
if [ -n "$BEFORE_SHA" ]; then
git fetch --no-tags --depth=1 origin "$BEFORE_SHA"
fi
fetch-depth: 0

- name: Set up CI environment
id: environment
uses: ./.github/actions/ci-environment
with:
uv-version: ${{ env.CI_UV_VERSION }}
runner-label: ubuntu-latest
lookup-only: "true"
sync-dependencies: "false"

- name: Detect release
id: release
- name: Publish release
env:
BEFORE_SHA: ${{ github.event.before }}
HEAD_SHA: ${{ github.sha }}
GH_TOKEN: ${{ github.token }}
RELEASE_REPOSITORY: ${{ github.repository }}
RELEASE_COMMIT: ${{ inputs.release_commit || github.sha }}
CI_PYTHON: ${{ steps.environment.outputs.python-version }}
run: |
uv run --no-project --python "$(cat .python-version)" python - <<'PY'
import os
import subprocess
import tomllib
from pathlib import Path

head = os.environ["HEAD_SHA"]
before = os.environ.get("BEFORE_SHA") or f"{head}^"
def version(revision):
result = subprocess.run(
["git", "show", f"{revision}:pyproject.toml"],
check=True, capture_output=True, text=True,
)
return tomllib.loads(result.stdout)["project"]["version"]
current, previous = version(head), version(before)
with Path(os.environ["GITHUB_OUTPUT"]).open("a", encoding="utf-8") as output:
output.write(f"changed={str(current != previous).lower()}\n")
print(f"Release version: {previous} -> {current}")
PY

- name: Build and validate release
if: steps.release.outputs.changed == 'true'
run: |
uv build --no-sources --out-dir "$RUNNER_TEMP/fcc-release"
uv run --no-project --python "$(cat .python-version)" python scripts/validate_release.py "$RUNNER_TEMP/fcc-release"

- name: Publish release
if: steps.release.outputs.changed == 'true'
run: uv publish --trusted-publishing always "$RUNNER_TEMP/fcc-release/"*
uv run --no-project --python "$CI_PYTHON" python -m scripts.publish_release \
--repository "$RELEASE_REPOSITORY" --commit "$RELEASE_COMMIT"
41 changes: 5 additions & 36 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,37 +21,6 @@ concurrency:
cancel-in-progress: true

jobs:
version-policy:
name: Version policy
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0

- name: Fetch target revision
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
BASE_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name }}
run: git fetch --no-tags "https://github.com/$BASE_REPOSITORY.git" "$BASE_SHA"

- name: Set up CI environment
uses: ./.github/actions/ci-environment
with:
uv-version: ${{ env.CI_UV_VERSION }}
runner-label: ubuntu-latest

- name: Check version policy
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: uv run --locked --group dev python scripts/check_version_policy.py --base "$BASE_SHA" --head "$HEAD_SHA"

no-suppressions-or-legacy-annotations:
name: Ban suppressions and legacy annotations
runs-on: ubuntu-latest
Expand All @@ -63,7 +32,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
fetch-depth: 1
fetch-depth: 0

- name: "Fail on type ignores and legacy future annotations"
run: |
Expand Down Expand Up @@ -94,7 +63,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
fetch-depth: 1
fetch-depth: 0

- name: Set up CI environment
uses: ./.github/actions/ci-environment
Expand Down Expand Up @@ -126,7 +95,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
fetch-depth: 1
fetch-depth: 0

- name: Set up CI environment
uses: ./.github/actions/ci-environment
Expand Down Expand Up @@ -159,7 +128,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
fetch-depth: 1
fetch-depth: 0

- name: Set up CI environment
uses: ./.github/actions/ci-environment
Expand All @@ -173,7 +142,7 @@ jobs:
- name: Run Playwright
run: >-
uv run --locked pytest e2e -v --tb=short
-o faulthandler_timeout=10 --durations=20
-o thread_dump_timeout=10 --durations=20
--tracing=retain-on-failure --screenshot=only-on-failure
--full-page-screenshot --output=test-results

Expand Down
56 changes: 17 additions & 39 deletions .github/workflows/validate-bug-report-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ jobs:
issue_number,
});
const fieldPattern = "^### FCC version\\r?\\n+\\s*([^\\r\\n]+)";
const versionPattern = "(?:^|[^0-9A-Za-z_.])v?([0-9]+\\.[0-9]+\\.[0-9]+)(?![0-9A-Za-z_+-]|\\.[0-9A-Za-z_])";
const projectVersionPattern = "^\\s*version\\s*=\\s*([\"'])([0-9]+\\.[0-9]+\\.[0-9]+)\\1\\s*(?:#.*)?$";
const releaseVersionPattern = "^v((?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*))$";
const versionPattern = "(?:^|[^0-9A-Za-z_.])v?([0-9]+\\.[0-9]+\\.[0-9]+(?:\\.dev[0-9]+(?:\\+g[0-9a-f]+(?:\\.d[0-9]{8})?)?|\\+d[0-9]{8})?)(?![0-9A-Za-z_+-]|\\.[0-9A-Za-z_])";
const match = (issue.body || "").match(new RegExp(fieldPattern, "m"));
const fieldValue = match?.[1]?.trim() || "";
const versionMatches = [...fieldValue.matchAll(new RegExp(versionPattern, "g"))];
Expand Down Expand Up @@ -70,22 +70,6 @@ jobs:
return false;
};

const projectVersionFromToml = (pyproject) => {
let inProjectSection = false;
for (const line of pyproject.split(/\r?\n/)) {
const stripped = line.trim();
if (stripped === "[project]") {
inProjectSection = true;
continue;
}
if (inProjectSection && stripped.startsWith("[")) return undefined;
if (!inProjectSection) continue;
const projectVersion = line.match(new RegExp(projectVersionPattern));
if (projectVersion !== null) return projectVersion[2];
}
return undefined;
};

if (!valid) {
await deleteManagedComment(outdatedComment);
if (!labels.has(labelName)) {
Expand All @@ -98,7 +82,7 @@ jobs:
...repo,
name: labelName,
color: "d4c5f9",
description: "FCC version must contain number.number.number or be None",
description: "FCC version must contain one installed version or be None",
});
} catch (createError) {
if (createError.status !== 422) throw createError;
Expand All @@ -114,7 +98,7 @@ jobs:
await github.rest.issues.createComment({
...repo,
issue_number,
body: `${invalidMarker}\nPlease edit **FCC version** so it contains one \`number.number.number\` value, such as \`1.22.333\`, or is exactly \`None\`. Run \`fcc-server --version\` to find the installed version.`,
body: `${invalidMarker}\nPlease edit **FCC version** so it contains one version copied from \`fcc-server --version\`, including any development suffix, or is exactly \`None\`.`,
});
}
return;
Expand All @@ -129,37 +113,31 @@ jobs:
}
await deleteManagedComment(invalidComment);

if (reportedVersion === undefined) {
// Checkout versions identify development source, not published releases.
if (reportedVersion === undefined || reportedVersion.includes(".dev") || reportedVersion.includes("+")) {
await deleteManagedComment(outdatedComment);
return;
}

const defaultBranch = context.payload.repository.default_branch;
const { data: versionFile } = await github.rest.repos.getContent({
...repo,
path: "pyproject.toml",
ref: defaultBranch,
});
if (
Array.isArray(versionFile)
|| versionFile.type !== "file"
|| versionFile.encoding !== "base64"
|| !versionFile.content
) {
throw new Error(`Could not read pyproject.toml from ${defaultBranch}`);
let release;
try {
({ data: release } = await github.rest.repos.getLatestRelease(repo));
} catch (error) {
if (error.status !== 404) throw error;
await deleteManagedComment(outdatedComment);
return;
}
const pyproject = Buffer.from(versionFile.content, "base64").toString("utf8");
const latestVersion = projectVersionFromToml(pyproject);
if (latestVersion === undefined) {
throw new Error(`Could not read the project version from ${defaultBranch}`);
const latestVersion = release.tag_name.match(new RegExp(releaseVersionPattern))?.[1];
if (!latestVersion || release.draft || release.prerelease) {
throw new Error("Latest release must have a stable vMAJOR.MINOR.PATCH tag");
}

if (!isOlderVersion(reportedVersion, latestVersion)) {
await deleteManagedComment(outdatedComment);
return;
}

const body = `${outdatedMarker}\nThis report uses FCC \`${reportedVersion}\`, while the current version on \`${defaultBranch}\` is \`${latestVersion}\`. Please [update FCC](${context.payload.repository.html_url}#install), restart FCC, and check whether the issue still occurs. The update may already contain the fix.`;
const body = `${outdatedMarker}\nThis report uses FCC \`${reportedVersion}\`, while the latest published version is \`${latestVersion}\`. Please [update FCC](${context.payload.repository.html_url}#update), restart FCC, and check whether the issue still occurs. The update may already contain the fix.`;
if (outdatedComment === undefined) {
await github.rest.issues.createComment({
...repo,
Expand Down
51 changes: 51 additions & 0 deletions .github/workflows/version-policy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Release intent

env:
CI_UV_VERSION: "0.12.13"
UV_MALWARE_CHECK: "1"
UV_PREVIEW_FEATURES: "malware-check"

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

concurrency:
group: release-intent-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
version-policy:
name: Version policy
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
pull-requests: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0

- name: Read current pull request
env:
GH_TOKEN: ${{ github.token }}
PR_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BASE_REPOSITORY_URL: ${{ github.server_url }}/${{ github.repository }}.git
run: |
gh api "repos/$PR_REPOSITORY/pulls/$PR_NUMBER" > "$RUNNER_TEMP/release-pr.json"
base=$(jq -r .base.sha "$RUNNER_TEMP/release-pr.json")
git fetch --no-tags "$BASE_REPOSITORY_URL" "$base"

- name: Set up CI environment
uses: ./.github/actions/ci-environment
with:
uv-version: ${{ env.CI_UV_VERSION }}
runner-label: ubuntu-latest

- name: Check version policy
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: uv run --locked python scripts/check_version_policy.py --head "$HEAD_SHA" --pr-json "$RUNNER_TEMP/release-pr.json"
1 change: 0 additions & 1 deletion .python-version

This file was deleted.

Loading
Loading