diff --git a/.editorconfig b/.editorconfig index adea89a8..6ee38ca1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -36,3 +36,6 @@ max_line_length = off [{*.yaml,*.yml}] indent_size = 2 + +[*.toml] +indent_size = 2 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2ad1a1da..10021a68 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,10 @@ -- [ ] Have you followed the guidelines for contributing? -- [ ] Have you signed the [CLA](http://www.ubuntu.com/legal/contributors/)? -- [ ] Have you successfully run `make lint && make test`? +Describe your changes. --- + +- [ ] I've followed the [contribution guidelines](https://github.com/canonical/craft-cli/blob/main/CONTRIBUTING.md). +- [ ] I've signed the [CLA](http://www.ubuntu.com/legal/contributors/). +- [ ] I've successfully run `make lint && make test`. +- [ ] I've added or updated any relevant documentation. +- [ ] In documents I changed, I [added a meta description](https://canonical-starflow.readthedocs-hosted.com/how-to/add-a-page-meta-description/) if one was missing. +- [ ] I've updated the relevant release notes. diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 00000000..df556c7c --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,28 @@ +# Configuration related to self-hosted runner. +self-hosted-runner: + # Labels of self-hosted runner in array of strings. + labels: + - spread-enabled + - spread-installed + # Architectures + - amd64 + - arm64 + - ppc64el + - riscv64 + - s390x + # Ubuntu releases + - focal + - jammy + - noble + - resolute + # Sizes + - medium + - large + - xlarge + - 2xlarge + - 2xlarge-extra + # Additional possible labels + - edge + - tiobe + - large-tiobe + - 2xlarge-tiobe diff --git a/.github/workflows/check-renovate.yaml b/.github/workflows/check-renovate.yaml index f9ce1bca..aa10fec1 100644 --- a/.github/workflows/check-renovate.yaml +++ b/.github/workflows/check-renovate.yaml @@ -17,17 +17,24 @@ on: jobs: renovate: runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Install node - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 + - name: Install uv + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 - name: Install renovate + # zizmor: ignore[adhoc-packages] renovate is installed ad-hoc here to validate its own config; there is no lockfile for this ephemeral tool. run: npm install --global re2 renovate - name: Enable ssh access - uses: mxschmitt/action-tmate@v3 + uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3.24 if: ${{ inputs.enable_ssh_access }} with: limit-access-to-actor: true diff --git a/.github/workflows/copilot-setup-steps.yaml b/.github/workflows/copilot-setup-steps.yaml new file mode 100644 index 00000000..bd67eb64 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yaml @@ -0,0 +1,75 @@ +name: Copilot Setup Steps + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yaml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yaml + +jobs: + copilot-setup-steps: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24 + cache: npm + cache-dependency-path: common.mk + + - name: Install uv + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + enable-cache: true + cache-dependency-glob: | + pyproject.toml + uv.lock + + - name: Install release tools + uses: taiki-e/install-action@8cca0918f8471c33515202b73f56ab27f2bed283 # v2.87.29 + with: + tool: | + osv-scanner + prek + shellcheck + shfmt + tombi + zizmor + fallback: none + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install --yes \ + libxml2-dev \ + libxslt1-dev + + - name: Install actionlint + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p "$HOME/.local/bin" + gh release download \ + --repo rhysd/actionlint \ + --pattern 'actionlint_*_linux_amd64.tar.gz' \ + --output /tmp/actionlint.tar.gz \ + --clobber + tar -xzf /tmp/actionlint.tar.gz -C "$HOME/.local/bin" actionlint + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Prepare git configuration + run: git config --local --unset-all core.hooksPath || true + + - name: Set up remaining repository tooling + run: make setup diff --git a/.github/workflows/policy.yaml b/.github/workflows/policy.yaml index 3b1383d4..76787ead 100644 --- a/.github/workflows/policy.yaml +++ b/.github/workflows/policy.yaml @@ -10,10 +10,19 @@ on: jobs: policy: permissions: - contents: read actions: read + contents: read security-events: write uses: canonical/starflow/.github/workflows/policy.yaml@main python-scans: name: Security scan + permissions: + contents: read uses: canonical/starflow/.github/workflows/scan-python.yaml@main + with: + osv-extra-args: "--config=osv-scanner.toml" + osv-exclude-paths: | + docs + uv-export-no-groups: | + docs + docs-sphinx-stack diff --git a/.github/workflows/release-publish.yaml b/.github/workflows/release-publish.yaml index 56625f87..bb4f53be 100644 --- a/.github/workflows/release-publish.yaml +++ b/.github/workflows/release-publish.yaml @@ -3,23 +3,28 @@ on: push: tags: # These tags should be protected, remember to enable the rule: - # https://github.com/canonical/craft-cli/settings/tag_protection + # https://github.com/canonical/starbase/settings/tag_protection - "[0-9]+.[0-9]+.[0-9]+" - -permissions: - contents: write + pull_request: + paths: + - .github/workflows/release-publish.yaml jobs: source-wheel: - runs-on: [self-hosted] + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) + runs-on: [self-hosted, medium, linux] + permissions: + contents: read steps: - name: Install uv run: | sudo snap install --classic astral-uv - sudo apt-get install --yes python3-venv - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Fetch tag annotations + if: ${{ github.event_name != 'pull_request' }} run: | # Note: we fetch the tags here instead of using actions/checkout's "fetch-tags" # because of https://github.com/actions/checkout/issues/1467 @@ -27,46 +32,50 @@ jobs: git describe --dirty --long --match '[0-9]*.[0-9]*.[0-9]*' --exclude '*[^0-9.]*' - name: Build packages run: | - uv venv --no-managed-python - uv pip install build twine - uv run -m build - uv run twine check dist/* + uv build + uvx twine check dist/* - name: Upload pypi packages artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: pypi-packages path: dist/ + # NOTE: This PyPI publishing job is often unnecessary for most applications + # that use starbase. Please ensure this job is excluded from merges/PRs + # originating from starbase unless explicitly required for a specific app. pypi: needs: ["source-wheel"] - runs-on: [self-hosted, amd64] + runs-on: [self-hosted, amd64, medium, linux] permissions: # IMPORTANT: this permission is mandatory for trusted publishing id-token: write steps: - name: Get packages - uses: actions/download-artifact@v8 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: pypi-packages path: dist/ - name: Publish to pypi + # Only actually push to pypi when we're on a tag. + if: ${{ startsWith(github.ref, 'refs/tags/') }} # Note: this action uses PyPI's support for Trusted Publishers # It needs a configuration on the PyPI project - see: # https://docs.pypi.org/trusted-publishers/adding-a-publisher/#github-actions - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 github-release: needs: ["source-wheel"] - runs-on: [self-hosted] + runs-on: [self-hosted, medium, linux] + permissions: + contents: write steps: - name: Get pypi artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: pypi-packages - name: Release - uses: softprops/action-gh-release@v3 - with: - # Generate release notes on the new GH release - generate_release_notes: true - # Add wheel and source tarball - files: | - *.whl - *.tar.gz + # Only actually publish the Github release when we're on a tag. + if: ${{ startsWith(github.ref, 'refs/tags/') }} + env: + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ github.ref_name }} + run: gh release create "$RELEASE_TAG" --generate-notes -- *.whl *.tar.gz diff --git a/.github/workflows/tics.yaml b/.github/workflows/tics.yaml index 1b9465b4..b4fdb426 100644 --- a/.github/workflows/tics.yaml +++ b/.github/workflows/tics.yaml @@ -3,12 +3,15 @@ on: push: branches: - "main" - # For debugging - - "work/tiobe*" + # For troubleshooting + - "work/tiobe**" jobs: TICS: + permissions: + contents: read uses: canonical/starflow/.github/workflows/tics.yaml@main with: project: "craft-cli" - secrets: inherit + secrets: + TICSAUTHTOKEN: ${{ secrets.TICSAUTHTOKEN }} diff --git a/.github/zizmor.yaml b/.github/zizmor.yaml new file mode 100644 index 00000000..74718389 --- /dev/null +++ b/.github/zizmor.yaml @@ -0,0 +1,5 @@ +rules: + unpinned-uses: + config: + policies: # Allow unpinned references to starflow. + canonical/starflow/*: ref-pin diff --git a/.gitignore b/.gitignore index 28596654..e2332962 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,8 @@ eggs/ .eggs/ lib/ lib64/ -parts/ +# Only top level parts dir, otherwise this conflicts with inner dirs called parts. +/parts/ sdist/ var/ wheels/ @@ -72,6 +73,8 @@ instance/ # Sphinx documentation docs/_build/ +docs/_linkcheck/ +docs/reference/gen/ docs/spelling/ # PyBuilder @@ -144,8 +147,12 @@ dmypy.json # Ignore version module generated by setuptools_scm /*/_version.py -# Visual Studio Code -.vscode/ - +# IDE configs +# VS Code/Codium +/.vscode/ # JetBrains IDEs /.idea/ +# Zed +/.zed/ +# KDevelop +/.kdev4/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd0090b2..28810776 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,5 +24,11 @@ repos: rev: "" # Intentionally blank, despite the warning. hooks: - id: prettier + files: ^(.\/?)*\.(yaml|yml|json|json5|css|md)$ additional_dependencies: - - prettier@3.4.2 + - prettier@3.6.0 # renovate: datasource=npm + - repo: https://github.com/scop/pre-commit-shfmt + rev: "v3.13.1-1" + hooks: + - id: shfmt + exclude: '\.sh\.j2$' diff --git a/AGENTS.md b/AGENTS.md index 6bc19c75..92455f4d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,6 +64,12 @@ make docs make lint-docs ``` +Run documentation targets from the repository root using `make docs-`; +never invoke the docs Makefile directly, with `make -C`, or equivalent, as it +bypasses required configuration. Use `make docs-help` to list targets. This +applies only to commands you run directly, not those triggered via repo +tooling. + ## Practices - Backward compatibility is a **hard requirement**. Apps using this library must diff --git a/Makefile b/Makefile index 21c903a6..f4c0c04f 100644 --- a/Makefile +++ b/Makefile @@ -10,10 +10,13 @@ UV_TICS_GROUPS := "--group=tics" include common.mk .PHONY: format -format: format-ruff format-codespell format-prettier ## Run all automatic formatters +format: format-ruff format-codespell format-prettier format-shfmt format-tombi format-pre-commit ## Run all automatic formatters .PHONY: lint -lint: lint-ruff lint-ty lint-codespell lint-prettier lint-shellcheck lint-docs lint-twine ## Run all linters +lint: lint-code lint-docs lint-twine lint-uv-lockfile lint-actions ## Run all linters + +.PHONY: lint-code +lint-code: lint-ruff lint-ty lint-codespell lint-prettier lint-shfmt lint-shellcheck lint-tombi ## Run code-specific linters .PHONY: pack pack: pack-pip ## Build all packages @@ -44,17 +47,3 @@ endif # If additional build dependencies need installing in order to build the linting env. .PHONY: install-lint-build-deps install-lint-build-deps: install-ty - -.PHONY: lint-ty -lint-ty: install-ty - ty check - -.PHONY: install-ty -install-ty: -ifneq ($(shell which ty),) -else ifneq ($(shell which snap),) - sudo snap install --beta astral-ty - sudo snap alias astral-ty.ty ty -else ifneq ($(shell which uv),) - uv tool install ty -endif diff --git a/common.mk b/common.mk index 45f46c49..c519fca6 100644 --- a/common.mk +++ b/common.mk @@ -2,6 +2,7 @@ # https://github.com/canonical/starbase SOURCES=$(wildcard *.py) $(PROJECT) tests + # Env vars for the docs Sphinx Stack. They must be exported so make can pass them to the # docs Makefile. export DOCS_BUILDDIR ?= _build @@ -18,7 +19,7 @@ else APT := apt-get endif -PRETTIER=npm exec --package=prettier -- prettier --log-level warn +PRETTIER=npm exec --package=prettier@3.6.0 -- prettier --log-level warn # renovate: datasource=npm PRETTIER_FILES="**/*.{yaml,yml,json,json5,css,md}" # Cutoff (in seconds) before a test is considered slow by pytest @@ -35,24 +36,12 @@ export UV_FROZEN := true .PHONY: help help: ## Show this help. - @printf "\e[1m%-30s\e[0m | \e[1m%s\e[0m\n" "Target" "Description" - printf "\e[2m%-30s + %-41s\e[0m\n" "------------------------------" "------------------------------------------------" - egrep '^[^:]+\: [^#]*##' $$(echo $(MAKEFILE_LIST) | tac --separator=' ') | sed -e 's/^[^:]*://' -e 's/:[^#]*/ /' | sort -V| awk -F '[: ]*' \ - '{ - if ($$2 == "##") - { - $$1=sprintf(" %-28s", $$1); - $$2=" | "; - print $$0; - } - else - { - $$1=sprintf(" └ %-25s", $$1); - $$2=" | "; - $$3=sprintf(" └ %s", $$3); - print $$0; - } - }' | uniq + @printf "\033[1m%-30s\033[0m | \033[1m%s\033[0m\n" "Target" "Description" + @printf "\033[2m%-30s + %-41s\033[0m\n" "------------------------------" "------------------------------------------------" + @cat $$(echo $(MAKEFILE_LIST) | tac --separator=' ' 2>/dev/null || echo $(MAKEFILE_LIST)) | grep -E '^[^[:space:]][^:]*\:[^#]*##' | \ + sed -e 's/:[^#]*/ /' | sort -V | \ + awk -F '[: ]+' '{ if ($$2 == "##") { $$1=sprintf(" %-28s", $$1); $$2=" | "; print $$0; } else { $$1=sprintf(" └ %-25s", $$1); $$2=" | "; $$3=sprintf(" └ %s", $$3); print $$0; } }' | \ + uniq .PHONY: setup setup: install-uv _setup-docs _setup-lint _setup-tests setup-precommit install-build-deps ## Set up a development environment @@ -76,7 +65,7 @@ setup-lint: _setup-lint ##- Set up a linting-only environment uv sync $(UV_LINT_GROUPS) .PHONY: _setup-lint -_setup-lint: install-uv install-shellcheck install-pyright install-lint-build-deps +_setup-lint: install-uv install-shellcheck install-shfmt install-lint-build-deps install-actionlint .PHONY: setup-tests setup-tests: _setup-tests ##- Set up a testing environment without linters @@ -120,10 +109,24 @@ format-ruff: install-ruff ##- Automatically format with ruff format-codespell: ##- Fix spelling issues with codespell uv run codespell --toml pyproject.toml --write-changes $(SOURCES) +.PHONY: format-pre-commit +format-pre-commit: ##- Format the entire repository using pre-commit + uv tool run pre-commit run + .PHONY: format-prettier format-prettier: install-npm ##- Format files with prettier $(PRETTIER) --write $(PRETTIER_FILES) +.PHONY: format-shfmt +format-shfmt: install-shfmt ##- Format shell scripts + @# jinja2 shell script templates are mistakenly counted as "true" shell scripts due to their shebang, + @# so explicitly filter them out + git ls-files -z | xargs -0 sh -c 'for f; do case "$$f" in *.sh.j2) continue;; esac; file --mime-type -Nn -- "$$f" | grep -q shellscript && printf "%s\0" "$$f"; done' -- | xargs -0r shfmt -w + +.PHONY: format-tombi +format-tombi: install-tombi ##- Format TOML files with tombi + tombi format + .PHONY: lint-ruff lint-ruff: install-ruff ##- Lint with ruff ifneq ($(CI),) @@ -145,26 +148,35 @@ ifneq ($(CI),) @echo ::endgroup:: endif -.PHONY: lint-mypy -lint-mypy: ##- Check types with mypy +.PHONY: lint-ty +lint-ty: install-ty ##- Check types with Astral ty ifneq ($(CI),) @echo ::group::$@ endif - uv run mypy --show-traceback --show-error-codes $(PROJECT) + ty check --python .venv $(SOURCES) ifneq ($(CI),) @echo ::endgroup:: endif -.PHONY: lint-pyright -lint-pyright: ##- Check types with pyright +.PHONY: lint-uv-lockfile +lint-uv-lockfile: install-uv ##- Check that uv.lock matches expectations from pyproject.toml ifneq ($(CI),) @echo ::group::$@ endif -ifneq ($(shell which pyright),) # Prefer the system pyright - pyright --pythonpath .venv/bin/python -else - uv tool run pyright --pythonpath .venv/bin/python + unset UV_FROZEN + uv lock --check +ifneq ($(CI),) + @echo ::endgroup:: +endif + +.PHONY: lint-shfmt +lint-shfmt: install-shfmt ##- Lint shell script formatting +ifneq ($(CI),) + @echo ::group::$@ endif + @# jinja2 shell script templates are mistakenly counted as "true" shell scripts due to their shebang, + @# so explicitly filter them out + git ls-files -z | xargs -0 sh -c 'for f; do case "$$f" in *.sh.j2) continue;; esac; file --mime-type -Nn -- "$$f" | grep -q shellscript && printf "%s\0" "$$f"; done' -- | xargs -0r shfmt --diff ifneq ($(CI),) @echo ::endgroup:: endif @@ -176,11 +188,12 @@ ifneq ($(CI),) endif @# jinja2 shell script templates are mistakenly counted as "true" shell scripts due to their shebang, @# so explicitly filter them out - git ls-files | grep -vE "\.sh\.j2$$" | file --mime-type -Nnf- | grep shellscript | cut -f1 -d: | xargs -r shellcheck + git ls-files -z | xargs -0 sh -c 'for f; do case "$$f" in *.sh.j2) continue;; esac; file --mime-type -Nn -- "$$f" | grep -q shellscript && printf "%s\0" "$$f"; done' -- | xargs -0r shellcheck ifneq ($(CI),) @echo ::endgroup:: endif + .PHONY: lint-prettier lint-prettier: install-npm ##- Lint files with prettier ifneq ($(CI),) @@ -191,7 +204,29 @@ ifneq ($(CI),) @echo ::endgroup:: endif +.PHONY: lint-tombi +lint-tombi: install-tombi ##- Check TOML formatting with tombi +ifneq ($(CI),) + @echo ::group::$@ +endif + tombi format --check --diff +ifneq ($(CI),) + @echo ::endgroup:: +endif +.PHONY: lint-actions +lint-actions: install-actionlint ##- Lint GitHub actions with actionlint +ifneq ($(CI),) + @echo ::group::$@ +endif + actionlint +ifneq ($(CI),) + @echo ::endgroup:: +endif + +# Legacy alias for linting docs +.PHONY: lint-docs +lint-docs: docs-lint ##- Lint the documentation .PHONY: lint-twine lint-twine: pack-pip ##- Lint Python packages with twine @@ -216,7 +251,7 @@ test-slow: ##- Run slow tests uv run pytest -m 'slow' .PHONY: test-coverage -test-coverage: ## Generate coverage report +test-coverage: ##- Generate coverage report ifeq ($(COVERAGE_SOURCE),) uv run coverage run --source $(PROJECT),tests -m pytest else @@ -329,10 +364,6 @@ ifneq ($(CI),) @echo ::endgroup:: endif -# Legacy alias for linting docs -.PHONY: lint-docs -lint-docs: docs-lint ##- Lint the documentation - .PHONY: pack-pip pack-pip: ##- Build packages for pip (sdist, wheel) ifneq ($(CI),) @@ -359,6 +390,17 @@ else curl -LsSf https://astral.sh/uv/install.sh | sh endif +.PHONY: install-actionlint +install-actionlint: +ifneq ($(shell which actionlint),) +else ifneq ($(shell which snap),) + sudo snap install actionlint +else ifneq ($(shell which brew),) + brew install actionlint +else + $(warning Actionlint not installed. Please install it yourself.) +endif + .PHONY: install-codespell install-codespell: ifneq ($(shell which codespell),) @@ -371,17 +413,6 @@ else $(warning Codespell not installed. Please install it yourself.) endif -.PHONY: install-pyright -install-pyright: install-uv -ifneq ($(shell which pyright),) -else ifneq ($(shell which snap),) - sudo snap install --classic pyright -else - # Workaround for a bug in npm - [ -d "$(HOME)/.npm/_cacache" ] && chown -R `id -u`:`id -g` "$(HOME)/.npm" || true - uv tool install pyright -endif - .PHONY: install-ruff install-ruff: ifneq ($(shell which ruff),) @@ -403,6 +434,40 @@ else $(warning Shellcheck not installed. Please install it yourself.) endif +.PHONY: install-shfmt +install-shfmt: +ifneq ($(shell which shfmt),) +else ifneq ($(shell which snap),) + sudo snap install shfmt +else ifneq ($(shell which brew),) + brew install shfmt +else + $(warning shfmt not installed. Please install it yourself.) +endif + +.PHONY: install-tombi +install-tombi: +ifneq ($(shell which tombi),) +else ifneq ($(shell which snap),) + sudo snap install --classic tombi +else ifneq ($(shell which brew),) + brew install tombi +else + make install-uv + uv tool install tombi +endif + +.PHONY: install-ty +install-ty: +ifneq ($(shell which ty),) +else ifneq ($(shell which snap),) + sudo snap install --beta astral-ty + sudo snap alias astral-ty.ty ty +else + make install-uv + uv tool install ty +endif + .PHONY: install-npm install-npm: ifneq ($(shell which npm),) diff --git a/docs/.custom_wordlist.txt b/docs/.custom_wordlist.txt index 8615e71f..d9b78f4d 100644 --- a/docs/.custom_wordlist.txt +++ b/docs/.custom_wordlist.txt @@ -35,7 +35,7 @@ freefont Furo genindex getter -github +GitHub GlobalArgument GPG Grafana @@ -146,4 +146,3 @@ xml YAML YouTube YYYY - diff --git a/docs/.gitignore b/docs/.gitignore index a4bc774b..f00f06f6 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -21,7 +21,7 @@ package*.json # Unrelated cache and config files .DS_Store -__pycache__/ +__pycache__ .idea/ .vscode/ diff --git a/docs/.sphinx/spellingcheck.yaml b/docs/.sphinx/spellingcheck.yaml deleted file mode 100644 index a8621bb6..00000000 --- a/docs/.sphinx/spellingcheck.yaml +++ /dev/null @@ -1,29 +0,0 @@ -matrix: - - name: rST files - aspell: - lang: en - d: en_GB - dictionary: - wordlists: - - .wordlist.txt - - .custom_wordlist.txt - output: .sphinx/.wordlist.dic - sources: - - _build/**/*.html|!_build/**/craft_cli/**|!_build/genindex/**|!_build/craft_cli.*/** - pipeline: - - pyspelling.filters.html: - comments: false - attributes: - - title - - alt - ignores: - - code - - pre - - spellexception - - link - - title - - div.relatedlinks - - strong.command - - div.visually-hidden - - img - - a.p-navigation__link diff --git a/docs/conf.py b/docs/conf.py index f8666a0c..d0c0e324 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,7 +24,7 @@ project = "Craft CLI" # Author name; used in the default copyright statement in the page footer -author = "Canonical Group Ltd" +author = "Canonical Ltd." # Version string in sidebar # TODO: Uncomment and replace 'starcraft' with the product's main module on release @@ -45,7 +45,7 @@ # Product favicon; shown in bookmarks, browser tabs, etc. # TODO: To customise the favicon, uncomment and update the next line. -# html_favicon = "_static/favicon.png" +# html_favicon = ".sphinx/_static/favicon.png" # Dictionary of values to pass into the Sphinx context for all pages: # https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_context diff --git a/docs/explanation/index.rst b/docs/explanation/index.rst index 698dc6a3..ecd7e8d9 100644 --- a/docs/explanation/index.rst +++ b/docs/explanation/index.rst @@ -1,9 +1,15 @@ +.. meta:: + :description: The explanatory documentation for Craft CLI. These explanations aid in understanding the concepts and relationships of Craft CLI as a complete system. + .. _explanation: .. _explanations: Explanation =========== +Explanations provide a wider perspective of Craft CLI. They aid in understanding the +concepts and relationships of Craft CLI as a complete system. + .. toctree:: :maxdepth: 1 diff --git a/docs/how-to/index.rst b/docs/how-to/index.rst index 4c802a57..d3aa128a 100644 --- a/docs/how-to/index.rst +++ b/docs/how-to/index.rst @@ -1,8 +1,13 @@ +.. meta:: + :description: The practical, goal-oriented documentation for Craft CLI. These guides provide directions for completing tasks and solving problems with Craft CLI. + .. _how-to-guides: How-to guides ============= +These pages provide directions for completing tasks and solving problems with Craft CLI. + .. toctree:: :maxdepth: 1 diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 6d779e58..3a98a3c5 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -1,8 +1,14 @@ +.. meta:: + :description: The reference documentation for Craft CLI. These references describe the structure and function of the individual components in Craft CLI. + .. _reference: Reference ========= +References describe the structure and function of the individual components in +Craft CLI. + .. toctree:: :maxdepth: 2 diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst index 7e07e5fa..6cc5efae 100644 --- a/docs/tutorials/index.rst +++ b/docs/tutorials/index.rst @@ -1,8 +1,16 @@ +.. meta:: + :description: The tutorials for Craft CLI. These tutorials are guided lessons that help you develop skills by using Craft CLI. + .. _tutorials: Tutorials ========= +The tutorials are guided lessons that help you develop skills by using Craft CLI. Each +is self-contained and delivers a working result that marks a learning milestone and is +an opportunity for further exploration. If you're a newcomer to Craft CLI, this is the +best way to start. + .. toctree:: :maxdepth: 1 diff --git a/osv-scanner.toml b/osv-scanner.toml new file mode 100644 index 00000000..7050ca40 --- /dev/null +++ b/osv-scanner.toml @@ -0,0 +1 @@ +# OSV Scanner configuration diff --git a/pyproject.toml b/pyproject.toml index 1e9102c0..bd0c7701 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,8 +36,11 @@ emitter = "craft_cli.pytest_plugin" [project.optional-dependencies] pytest = ["pytest>=8.4"] +# Applications should avoid CVEs in optional, direct dependencies by setting a minimum +# version in 'dependency-groups'. [dependency-groups] dev = [ + "build>=0.7.0", "coverage[toml]==7.16.2", "pytest>=9.0", "pytest-cov==7.1.0", @@ -48,6 +51,7 @@ docs = [ "pytest>=8.4", # pytest is imported by the code, which is parsed for api gen. "sphinx-lint==1.0.2", "sphinx-toolbox==4.3.1", + "sphinxext-rediraffe==0.3.0", { include-group = "docs-sphinx-stack" }, ] # We disable the unused but default SP dependencies slated for removal @@ -272,8 +276,11 @@ classmethod-decorators = ["pydantic.validator", "pydantic.root_validator"] "S103", # Allow `os.chmod` setting a permissive mask `0o555` on file or directory "S108", # Allow Probable insecure usage of temporary file or directory "PLR0913", # Allow many arguments for test functions (useful if we need many fixtures) + "PLR0917", # Allow many positional arguments for test functions (useful if we need many fixtures) "PLR2004", # Allow magic values in tests "SLF", # Allow accessing private members from tests. + "FBT001", # Boolean-typed positional arguments are common in tests (e.g. parametrize) + "FBT002", # Boolean default values are common in test helper functions ] "__init__.py" = [ "I001", # isort leaves init files alone by default, this makes ruff ignore them too. @@ -320,13 +327,23 @@ git_describe_command = [ [tool.uv] constraint-dependencies = [ - # Basic constraints to allow --resolution=lowest + # Basic constraints to allow --resolution=lowest. + # + # Libraries should avoid CVEs in direct and transitive dependencies by setting a + # minimum version in this section. + # This allows standalone builds to avoid CVEs while not placing undue constraints on + # downstream users of the library. "cffi>=1.15", - "iniconfig>=1.1.0", + "filelock>=3.20.3", + "gitpython>=3.1.50", "httplib2>=0.20.0", + "idna>=3.15", + "iniconfig>=1.1.0", "libnacl>=2.0", + "lxml>=6.1.0", "markdown>=3.0", "markupsafe>=2.0", + "msgpack>=1.2.1", "oauthlib>=3.0.0", "protobuf>=5.0", "pynacl>=1.5", @@ -335,19 +352,12 @@ constraint-dependencies = [ "pytz>=2020", "pyyaml>=5.0", "regex>=2021.11.10", + "requests>=2.33.0", "setuptools>=50", "sphinx-basic-ng>=1.0.0b1", + "starlette>=1.3.1", "tornado>=4.0", + "urllib3>=2.6.3", "webencodings>=0.4.0", "wheel>=0.38", - - # Restricted dependencies for OSV avoidance - "filelock>=3.20.3", - "gitpython>=3.1.50", - "idna>=3.15", - "lxml>=6.1.0", - "msgpack>=1.2.1", - "requests>=2.33.0", - "starlette>=1.3.1", - "urllib3>=2.6.3", ] diff --git a/tests/unit/test_messages_emitter.py b/tests/unit/test_messages_emitter.py index fa537eca..3e7e010f 100644 --- a/tests/unit/test_messages_emitter.py +++ b/tests/unit/test_messages_emitter.py @@ -90,8 +90,8 @@ def func(mode, *, greeting="default greeting", **kwargs): def emitter_methods( - init: bool, # noqa: FBT001 - stop: bool = True, # noqa: FBT001, FBT002 + init: bool, + stop: bool = True, exclude: list[str] | None = None, ) -> list[Callable[..., Any]]: """Provide a list of all public methods on an Emitter object. diff --git a/uv.lock b/uv.lock index cbb67be4..b97554cc 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.13'", @@ -151,6 +151,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, ] +[[package]] +name = "build" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/67/4898a44ea4f3f8e213b0954ec0aa0a16971d62a6212d6ea3931e97115b99/build-1.6.1.tar.gz", hash = "sha256:51cc11666391ab6f092070437ac747002ff46f3e4113a3622177ee6b488bfc53", size = 113427, upload-time = "2026-09-10T07:56:00.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/9b/9fb3585dabcd73a1b2a6267f63f62649347c9e6d072c9fde365b105abb2c/build-1.6.1-py3-none-any.whl", hash = "sha256:ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7", size = 31179, upload-time = "2026-09-10T07:55:59.148Z" }, +] + [[package]] name = "cachecontrol" version = "0.14.4" @@ -540,6 +556,7 @@ pytest = [ [package.dev-dependencies] dev = [ + { name = "build" }, { name = "coverage", extra = ["toml"] }, { name = "pytest" }, { name = "pytest-cov" }, @@ -566,6 +583,7 @@ docs = [ { name = "sphinx-toolbox" }, { name = "sphinxcontrib-jquery" }, { name = "sphinxext-opengraph" }, + { name = "sphinxext-rediraffe" }, { name = "vale" }, ] docs-sphinx-stack = [ @@ -613,6 +631,7 @@ provides-extras = ["pytest"] [package.metadata.requires-dev] dev = [ + { name = "build", specifier = ">=0.7.0" }, { name = "coverage", extras = ["toml"], specifier = "==7.16.2" }, { name = "pytest", specifier = ">=9.0" }, { name = "pytest-cov", specifier = "==7.1.0" }, @@ -638,6 +657,7 @@ docs = [ { name = "sphinx-toolbox", specifier = "==4.3.1" }, { name = "sphinxcontrib-jquery", specifier = "~=4.1" }, { name = "sphinxext-opengraph", specifier = "~=0.13" }, + { name = "sphinxext-rediraffe", specifier = "==0.3.0" }, { name = "vale", specifier = "~=3.13" }, ] docs-sphinx-stack = [ @@ -825,6 +845,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl", hash = "sha256:ea0c9a0384df69ed86a943a15cde37d0360b82491b3910dc2215e202e62b5b02", size = 14794, upload-time = "2026-08-24T12:35:12.548Z" }, ] +[[package]] +name = "importlib-metadata" +version = "9.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/7e/1e7e8dc30634b93ebb3d58a3dea569ad146e656218d3960ab04f62047b29/importlib_metadata-9.0.1.tar.gz", hash = "sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99", size = 59124, upload-time = "2026-08-28T15:30:34.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/55/ecca97ae19075f1fac62def77731e7f535e6c1fb8f92ff08160c5e6dade8/importlib_metadata-9.0.1-py3-none-any.whl", hash = "sha256:bba5600596a7e21f3eef53281cf28d6a5195634d2f2b78ff9501a3272c6eaab0", size = 27920, upload-time = "2026-08-28T15:30:33.433Z" }, +] + [[package]] name = "iniconfig" version = "2.3.1" @@ -1321,6 +1353,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/ba/62b18e2e157d05f976bd490b349b55d8850c2a454a5c313c91af9d2e0baf/pylint-4.1.3-py3-none-any.whl", hash = "sha256:a85357cae24f33ad8d86c8f3daaa92c600ae4012b54a57299cee76000e9364cf", size = 588959, upload-time = "2026-10-11T07:49:08.272Z" }, ] +[[package]] +name = "pyproject-hooks" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/5d/f2ddeef4a855a102aaae5e97826a0260007522ab504421b75addfdb1517c/pyproject_hooks-1.3.3.tar.gz", hash = "sha256:defda19b854fa0d3bd4f76ea4ddcba8abd7dcfcdd585a6690ade050744fc5f43", size = 21013, upload-time = "2026-09-16T08:58:03.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/11/044d1ae1b4ec0d7af88ee5bc91e081be1022533032b906a7bdabdbb60977/pyproject_hooks-1.3.3-py3-none-any.whl", hash = "sha256:5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b", size = 10724, upload-time = "2026-09-16T08:58:02.96Z" }, +] + [[package]] name = "pytest" version = "9.1.1" @@ -2118,6 +2159,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/a4/66c1fd4f8fab88faf71cee04a945f9806ba0fef753f2cfc8be6353f64508/sphinxext_opengraph-0.13.0-py3-none-any.whl", hash = "sha256:936c07828edc9ad9a7b07908b29596dc84ed0b3ceaa77acdf51282d232d4d80e", size = 1004152, upload-time = "2025-08-29T12:20:29.072Z" }, ] +[[package]] +name = "sphinxext-rediraffe" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/a9/ab13d156049eea633f992424f3e92cb40e3f1b606bb6d01d40a27457d38a/sphinxext_rediraffe-0.3.0.tar.gz", hash = "sha256:f319b3ccb7c3c3b6f63ffa6fd3eeb171b6d272df55075a9e84364394f391f507", size = 22114, upload-time = "2025-09-28T15:31:53.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/55/ab40a0d1378ee5c859590a633052cf1d0a1f8435af87558a9f7cd576601a/sphinxext_rediraffe-0.3.0-py3-none-any.whl", hash = "sha256:f4220beafa99c99177488276b8e4fcf61fbeeec4253c1e4aae841a18c475330c", size = 7194, upload-time = "2025-09-28T15:31:52.388Z" }, +] + [[package]] name = "standard-imghdr" version = "3.10.14" @@ -2663,3 +2716,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/a1/459ab96c5cda8a2164f594be6dc9f868de7971e6abafa696ea07534139a6/websockets-17.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:063508ce9e0db745f30ab52fc652f4e59efc79c2b74934b3837d5cdb974da620", size = 218612, upload-time = "2026-10-03T14:56:50.287Z" }, { url = "https://files.pythonhosted.org/packages/8a/58/835cd51934d6780fa586f275b5d9901eead6d81569b4343b3767cdbaae4c/websockets-17.2-py3-none-any.whl", hash = "sha256:6aa59f0ef92e796b2db6f5f26550c4713c0e4036899fadf02f55e2ed4db0b7ae", size = 211883, upload-time = "2026-10-03T14:56:51.898Z" }, ] + +[[package]] +name = "zipp" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/23/655a1802fe8041302c959774ca7c80b53bc24737ff3ef45cb50ef11bd96c/zipp-4.1.1.tar.gz", hash = "sha256:7ebb7a44c021b29fd8dbd7cce6812d0d7b5b454521f93cc71af6ccd155aaa70b", size = 27649, upload-time = "2026-10-03T17:03:03.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/98/df615823cd9419131ce19fba00de53a663794369e198aade064a244b385d/zipp-4.1.1-py3-none-any.whl", hash = "sha256:8979f52d874162f485ff2981e3891f3a3317b7a3dd43ff1e1775b9304f307a9c", size = 10582, upload-time = "2026-10-03T17:03:02.506Z" }, +]