Skip to content
Open
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
6 changes: 5 additions & 1 deletion .devcontainer/Dockerfile.AZL-3.0
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ FROM mcr.microsoft.com/azurelinux/base/core:3.0

# Install required packages
RUN tdnf -y update && \
tdnf -y install ca-certificates dnf dnf-utils which gh git golang gawk tar shadow-utils sudo tree bash-completion moby-engine moby-cli build-essential && \
tdnf -y install ca-certificates dnf dnf-utils which gh git golang gawk tar shadow-utils sudo tree bash-completion moby-engine moby-cli build-essential python3-pip nodejs && \
tdnf clean all

# Install ruff and pyright for Python linting/formatting/type-checking
# (used by 'mage check python' / 'mage fix python'). pyright requires node (installed above).
RUN pip3 install --no-cache-dir ruff pyright

# Define the 'll' alias for all users
RUN echo 'alias ll="ls -lah"' >> /etc/profile.d/aliases.sh && \
chmod +x /etc/profile.d/aliases.sh && \
Expand Down
4 changes: 4 additions & 0 deletions .devcontainer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The dev container uses the `azl_dev_3.0` image defined in `Dockerfile.AZL-3.0`.
- Go development tools,
- Mage build system,
- golangci-lint,
- ruff and pyright (plus Node.js, required by pyright) for Python linting/formatting/type-checking,
- required development dependencies for VS Code.

## Getting Started
Expand Down Expand Up @@ -47,6 +48,9 @@ The dev container automatically installs these VS Code extensions:
- JSON Language Features - JSON editing support
- markdownlint - Markdown linting
- YAML Language Support - YAML editing support
- Ruff (charliermarsh.ruff) - Python linting/formatting
- Pylance (ms-python.vscode-pylance) - Python type checking
- EditorConfig (editorconfig.editorconfig) - EditorConfig support

## Customization

Expand Down
7 changes: 6 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@
"dockerfile": "Dockerfile.AZL-3.0",
"context": "."
},
"runArgs": ["--privileged"],
"runArgs": [
"--privileged"
],
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {},
// Configure tool-specific properties.
"customizations": {
"vscode": {
"extensions": [
"charliermarsh.ruff",
"davidanson.vscode-markdownlint",
"editorconfig.editorconfig",
"github.copilot-chat",
"github.copilot",
"github.vscode-pull-request-github",
"golang.go",
"ms-python.vscode-pylance",
"ms-vscode.vscode-json",
"redhat.vscode-yaml"
],
Expand Down
35 changes: 35 additions & 0 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: "Python lint and type-check"
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
schedule:
# Run every night at 3:15am PST (11:15am UTC)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why do we need the linters to run on as schedule? Is it not enough to have them run on push?

Copy link
Copy Markdown
Contributor Author

@dmcilvaney dmcilvaney Jun 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have it on the others, but probably don't truly need it. Mostly it catches if the linters themselves change.

- cron: "15 11 * * *"

# Cancel in-progress runs of this workflow if a new run is triggered.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

permissions: {}

jobs:
python:
name: "Lint and type-check"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Get go tools
uses: ./.github/getgotools
- name: Install ruff and pyright
# ubuntu-latest ships Python and Node.js; pyright requires Node.
run: pip install ruff pyright
- name: Run mage check python
run: mage -v check python
7 changes: 7 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"recommendations": [
"charliermarsh.ruff",
"editorconfig.editorconfig",
"ms-python.vscode-pylance"
]
}
22 changes: 20 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,26 @@
"go.buildTags": "scenario",
"go.lintTool": "golangci-lint-v2",
"go.testTimeout": "5m",
"go.testFlags": ["-v"],
"go.testFlags": [
"-v"
],
// Lint rules for python
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit",
"source.organizeImports.ruff": "explicit",
"source.fixAll": "explicit",
"source.fixAll.pylance": "explicit"
}
},
"python.analysis.diagnosticsSource": "Pylance",
"python.missingPackage.severity": "Error",
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"ruff.configuration": "${workspaceFolder}/ruff.toml",
"ruff.lint.enable": true,
"cSpell.words": [
"acarl",
"acobaugh",
Expand Down Expand Up @@ -132,4 +151,3 @@
"wrapcheck"
]
}

10 changes: 10 additions & 0 deletions docs/developer/how-to/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
go install github.com/magefile/mage@latest
```

- **ruff** and **pyright** *(only if you touch Python code)* - Python linter/formatter
and type checker, used by `mage check python` / `mage fix python`. pyright requires
Node.js. The dev container installs all of these for you.

```bash
# On Azure Linux
tdnf install -y python3-pip nodejs
pip3 install ruff pyright
```

## Initial Setup

1. Fork the repository (strongly recommended for all contributors)
Expand Down
11 changes: 11 additions & 0 deletions docs/developer/reference/coding-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@

- See [azldev command guidelines](../reference/command-guidelines.md) for details on command structure and naming conventions.

## Python Code Style

Some functionality is implemented in Python scripts (e.g. scripts embedded in the
Go binary).

- **Linting & formatting**: All Python must pass `ruff` (config: [`ruff.toml`](../../../ruff.toml)).
Run `mage check python` to lint and `mage fix python` to auto-format and apply fixes.
- **Type checking**: All Python must pass `pyright` (config: [`pyrightconfig.json`](../../../pyrightconfig.json)).
This is also run by `mage check python`.
- Both checks are part of `mage check all`.

## Logging Guidelines

- Use structured logging with the `slog` package
Expand Down
39 changes: 24 additions & 15 deletions internal/app/azldev/core/sources/render_process.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""Process RPM specs inside a mock chroot: run rpmautospec and spectool for each
component, writing per-component results to a JSON file in the staging directory.
r"""Run rpmautospec and spectool for each component inside a mock chroot.

Writes per-component results to a JSON file in the staging directory.

This script is embedded in the azldev Go binary and executed inside a mock chroot
during ``azldev component render``. It avoids the need for complex inline shell
Expand All @@ -20,35 +21,41 @@
Results are written to ``<staging_dir>/results.json``::

[
{"name": "curl", "specFiles": "Source0: curl-8.5.0.tar.xz\\nPatch0: fix.patch", "error": null},
{"name": "curl", "specFiles": "Source0: curl-8.5.0.tar.xz\nPatch0: fix.patch", "error": null},
{"name": "broken", "specFiles": "", "error": "rpmautospec failed: ..."}
]

Progress is reported to stderr as ``PROGRESS <completed>/<total> <name>``.
"""

from __future__ import annotations

import json
import os
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path

# Number of positional arguments expected on the command line (script, staging_dir, max_workers).
EXPECTED_ARG_COUNT = 3


def process_component(staging_dir: str, name: str, spec_filename: str) -> dict:
def process_component(staging_dir: str, name: str, spec_filename: str) -> dict[str, str | None]:
"""Run rpmautospec + spectool for a single component, returning a result dict.

Trust boundary: name and spec_filename are validated by BatchProcess in
mockprocessor.go (validateComponentInput rejects path separators, empty
values, and non-basename spec filenames) before this script is invoked.
"""
comp_dir = os.path.join(staging_dir, name)
spec_path = os.path.join(comp_dir, spec_filename)
comp_dir = Path(staging_dir) / name
spec_path = comp_dir / spec_filename

# rpmautospec: expand %autorelease / %autochangelog in-place.
rpa_result = subprocess.run(
["rpmautospec", "process-distgit", spec_path, spec_path],
["rpmautospec", "process-distgit", str(spec_path), str(spec_path)],
capture_output=True,
text=True,
check=False,
)

if rpa_result.returncode != 0:
Expand All @@ -66,10 +73,11 @@ def process_component(staging_dir: str, name: str, spec_filename: str) -> dict:
f"_sourcedir {comp_dir}",
"-l",
"-a",
spec_path,
str(spec_path),
],
capture_output=True,
text=True,
check=False,
)

if st_result.returncode != 0:
Expand All @@ -83,15 +91,16 @@ def process_component(staging_dir: str, name: str, spec_filename: str) -> dict:


def main() -> int:
if len(sys.argv) != 3:
"""Read inputs.json, process every component in parallel, and write results.json."""
if len(sys.argv) != EXPECTED_ARG_COUNT:
print(f"usage: {sys.argv[0]} <staging_dir> <max_workers>", file=sys.stderr)
return 1

staging_dir = sys.argv[1]
max_workers = int(sys.argv[2])
inputs_path = os.path.join(staging_dir, "inputs.json")
inputs_path = Path(staging_dir) / "inputs.json"

with open(inputs_path) as f:
with inputs_path.open() as f:
inputs = json.load(f)

# Mark all paths as git-safe (ownership mismatch between host and chroot).
Expand Down Expand Up @@ -123,7 +132,7 @@ def main() -> int:
name = futures[future]
try:
completed_results[name] = future.result()
except Exception as exc:
except Exception as exc: # noqa: BLE001 - record any worker failure as a per-component error
completed_results[name] = {
"name": name,
"specFiles": "",
Expand All @@ -138,9 +147,9 @@ def main() -> int:
# Write results to a file in the staging directory rather than stdout.
# This avoids bufio.Scanner token size limits in the Go caller, which
# would truncate large JSON payloads (e.g., 7k components ≈ 560KB).
results_path = os.path.join(staging_dir, "results.json")
results_path = Path(staging_dir) / "results.json"

with open(results_path, "w") as results_file:
with results_path.open("w") as results_file:
json.dump(results, results_file)

return 0
Expand Down
Loading
Loading