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
51 changes: 51 additions & 0 deletions .agents/skills/release/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
name: release
description: Prepare, validate, and create an Up3date release tag by following docs/RELEASE.md. Use when the user asks to prepare, tag, publish, retry, or troubleshoot a project release.
---

# Release

Prepare and create an Up3date release tag using the repository's release
runbook as the authoritative procedure.

## Workflow

1. Locate the repository root and read `docs/RELEASE.md` completely before
taking any release-related action. Follow its current contents even when
they differ from this summary.
2. Inspect the current branch, working tree, remotes, version declarations,
lockfile, changelog, existing tags, and relevant GitHub release state.
Do not modify or discard unrelated user changes.
3. Determine the intended semantic version without a `v` prefix. If the user
has not supplied it and it cannot be unambiguously inferred from completed
release preparation, ask which version to release.
4. Prepare and synchronize the version and release notes exactly as required
by `docs/RELEASE.md`. Run the prescribed local quality checks and resolve
failures that are within the requested release scope.
5. Stop before tagging if the release commit is not on `main`, the local
`main` is not current, the worktree is dirty, required checks have failed,
versions disagree, release notes are missing, or the tag/release already
exists. Explain the concrete blocker instead of bypassing a safeguard.
6. Summarize the version, target commit, validation results, and the command
that will publish the tag. Obtain explicit user confirmation immediately
before creating or pushing the tag unless the user's current request
already explicitly authorizes creating and pushing that exact tag.
7. Create and push the release tag only through the helper specified by
`docs/RELEASE.md`. Do not manually create a GitHub release; the tag-driven
workflow owns that step.
8. Report the pushed tag and the release workflow result or link when it is
available. If the workflow fails, diagnose it and follow the runbook's
recovery rules.

## Safety Rules

- Never force-update a release tag.
- Never delete or recreate a tag unless the user explicitly authorizes the
exact recovery operation and the runbook permits it.
- Treat a published release tag as immutable.
- Do not bypass branch protection, tag protection, required checks, or
version synchronization.
- Do not invent release notes. Derive them from the actual changes and keep
them consistent with the existing changelog style, asking the user when a
product-facing description requires judgment.
- If `docs/RELEASE.md` is absent or contradictory, stop and report the issue.
5 changes: 5 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Approval from either owner is sufficient for repository-wide changes.
* @konmast3r @liberostelios

# Keep ownership explicit for changes to the ownership policy itself.
/.github/CODEOWNERS @konmast3r @liberostelios
264 changes: 264 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
name: Code quality

on:
push:
branches:
- main
pull_request:

permissions:
contents: read

concurrency:
group: quality-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
quality:
name: Ruff, typing, and unit tests
runs-on: ubuntu-latest
continue-on-error: false

defaults:
run:
shell: bash --noprofile --norc -euo pipefail {0}

steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Install uv and Python
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
python-version: "3.13"

- name: Install dependencies
run: uv sync --locked --dev

- name: Check formatting
id: formatting
continue-on-error: true
run: uv run ruff format --check --diff .

- name: Lint, imports, naming, and annotations
id: linting
continue-on-error: true
run: uv run ruff check --output-format=github .

- name: Enforce city_object spelling
id: city_object_spelling
continue-on-error: true
run: |
if grep -RInE --include='*.py' '(^|[^[:alnum:]_])[a-z_]*cityobjects?[a-z_]*([^[:alnum:]_]|$)' addon.py core models tests __init__.py; then
echo 'Use city_object or city_objects in Python identifiers.'
exit 1
fi

- name: Type check
id: type_checking
continue-on-error: true
run: uv run mypy

- name: Run unit tests
id: unit_tests
continue-on-error: true
env:
COVERAGE_FILE: .coverage.unit
run: >-
uv run pytest
--cov=.
--cov-branch
--cov-report=
--cov-fail-under=0

- name: Upload unit coverage
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: unit-coverage
path: .coverage.unit
include-hidden-files: true
if-no-files-found: ignore
retention-days: 1

- name: Report quality results
if: always()
env:
FORMATTING_RESULT: ${{ steps.formatting.outcome }}
LINTING_RESULT: ${{ steps.linting.outcome }}
SPELLING_RESULT: ${{ steps.city_object_spelling.outcome }}
TYPE_CHECKING_RESULT: ${{ steps.type_checking.outcome }}
UNIT_TESTS_RESULT: ${{ steps.unit_tests.outcome }}
run: |
{
echo '## Code quality results'
echo '| Check | Result |'
echo '| --- | --- |'
echo "| Formatting | ${FORMATTING_RESULT} |"
echo "| Linting | ${LINTING_RESULT} |"
echo "| city_object spelling | ${SPELLING_RESULT} |"
echo "| Type checking | ${TYPE_CHECKING_RESULT} |"
echo "| Unit tests | ${UNIT_TESTS_RESULT} |"
} >> "${GITHUB_STEP_SUMMARY}"

for result in \
"${FORMATTING_RESULT}" \
"${LINTING_RESULT}" \
"${SPELLING_RESULT}" \
"${TYPE_CHECKING_RESULT}" \
"${UNIT_TESTS_RESULT}"; do
if [[ "${result}" != 'success' ]]; then
exit 1
fi
done

blender_integration:
name: Blender ${{ matrix.blender_version }} integration tests
needs: quality
runs-on: ubuntu-latest
continue-on-error: false

strategy:
fail-fast: false
matrix:
include:
- blender_version: "5.2.0"
blender_series: "5.2"
blender_sha256: "96f6c181a30f4950607839dc84d42a354b250d8a0231b098b59b7bc69c351c48"

defaults:
run:
shell: bash --noprofile --norc -euo pipefail {0}

steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Install uv and Python
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
python-version: "3.13"

- name: Install dependencies
run: uv sync --locked --dev

- name: Install Blender ${{ matrix.blender_version }}
id: blender_setup
continue-on-error: true
env:
BLENDER_VERSION: ${{ matrix.blender_version }}
BLENDER_SERIES: ${{ matrix.blender_series }}
BLENDER_SHA256: ${{ matrix.blender_sha256 }}
run: |
blender_directory="blender-${BLENDER_VERSION}-linux-x64"
blender_archive="${RUNNER_TEMP}/${blender_directory}.tar.xz"
curl --fail --location --silent --show-error \
"https://download.blender.org/release/Blender${BLENDER_SERIES}/${blender_directory}.tar.xz" \
--output "${blender_archive}"
echo "${BLENDER_SHA256} ${blender_archive}" \
| sha256sum --check --strict
tar --extract --file "${blender_archive}" --directory "${RUNNER_TEMP}"
echo "BLENDER_EXECUTABLE=${RUNNER_TEMP}/${blender_directory}/blender" \
>> "${GITHUB_ENV}"

- name: Run headless Blender integration tests
id: blender_integration
continue-on-error: true
env:
COVERAGE_FILE: .coverage.blender
PYTHONPATH: ${{ github.workspace }}/.venv/lib/python3.13/site-packages
run: >-
"${BLENDER_EXECUTABLE}"
--background
--factory-startup
--python-use-system-env
--python-exit-code 1
--python tests/blender/run_integration_tests.py

- name: Upload Blender coverage
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: blender-coverage-${{ matrix.blender_version }}
path: .coverage.blender
include-hidden-files: true
if-no-files-found: ignore
retention-days: 1

- name: Report Blender integration results
if: always()
env:
BLENDER_SETUP_RESULT: ${{ steps.blender_setup.outcome }}
BLENDER_INTEGRATION_RESULT: ${{ steps.blender_integration.outcome }}
run: |
{
echo '## Blender ${{ matrix.blender_version }} integration results'
echo '| Check | Result |'
echo '| --- | --- |'
echo "| Blender setup | ${BLENDER_SETUP_RESULT} |"
echo "| Headless Blender integration | ${BLENDER_INTEGRATION_RESULT} |"
} >> "${GITHUB_STEP_SUMMARY}"

for result in \
"${BLENDER_SETUP_RESULT}" \
"${BLENDER_INTEGRATION_RESULT}"; do
if [[ "${result}" != 'success' ]]; then
exit 1
fi
done

coverage:
name: Combined test coverage
needs: [quality, blender_integration]
runs-on: ubuntu-latest

defaults:
run:
shell: bash --noprofile --norc -euo pipefail {0}

steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Install uv and Python
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
python-version: "3.13"

- name: Install dependencies
run: uv sync --locked --dev

- name: Download unit coverage
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: unit-coverage
path: .coverage-inputs/unit

- name: Download Blender coverage
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: blender-coverage-*
path: .coverage-inputs/blender
merge-multiple: true

- name: Check combined test coverage
run: |
uv run coverage combine .coverage-inputs/unit .coverage-inputs/blender

if uv run coverage report | tee coverage-report.txt; then
coverage_status=0
else
coverage_status=$?
fi

{
echo '## Combined test coverage'
echo '```text'
cat coverage-report.txt
echo '```'
} >> "${GITHUB_STEP_SUMMARY}"

exit "${coverage_status}"
Loading
Loading