diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..1bb3e01 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,9 @@ +# See here for image contents: https://github.com/devcontainers/images/tree/main/src/base-ubuntu + +# [Choice] Ubuntu version: noble, jammy, focal +ARG VARIANT="jammy" +FROM mcr.microsoft.com/devcontainers/base:${VARIANT} + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends diff --git a/.devcontainer/apt-packages.txt b/.devcontainer/apt-packages.txt new file mode 100644 index 0000000..be759c4 --- /dev/null +++ b/.devcontainer/apt-packages.txt @@ -0,0 +1,12 @@ +coreutils +direnv +figlet +findutils +gh +git +links +mawk +minizinc +sed +time +vim diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 71d9a3f..262360b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,47 +1,58 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu { - // "build": { - // "dockerfile": "Dockerfile", - // Update 'VARIANT' to pick an Ubuntu version: jammy / ubuntu-22.04, focal / ubuntu-20.04, bionic /ubuntu-18.04 - // Use ubuntu-22.04 or ubuntu-18.04 on local arm64/Apple Silicon. - // "args": { "VARIANT": "ubuntu-22.04" } - // }, - // Configure tool-specific properties. - "customizations": { - "vscode": { - "extensions": [ - "DavidAnson.vscode-markdownlint", - "GitHub.copilot", - "GitHub.copilot-chat", - "elagil.pre-commit-helper", - "formulahendry.code-runner", - "formulahendry.code-runner-copilot", - "github.vscode-github-actions", - "ms-python.python", - "vscodevim.vim" - ], - } - }, - // Features to add to the dev container. More info: https://containers.dev/features. - "features": { - // "ghcr.io/maks1ms/devcontainers-features/wine:0": {}, - "ghcr.io/devcontainers-contrib/features/actionlint:1": {}, - "ghcr.io/devcontainers-extra/features/pipx-package:1": {}, - "ghcr.io/devcontainers/features/python:1": {}, - "ghcr.io/guiyomh/features/vim:0": {}, - "ghcr.io/jungaretti/features/ripgrep:1": {}, - "ghcr.io/devcontainers/features/docker-in-docker:2": {} - }, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "uname -a", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/base:jammy", - "name": "dotfiles", - // Install dependencies and pre-commit hooks. - "postCreateCommand": "pip install -r .devcontainer/requirements.txt && pre-commit install", - // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode" + "build": { + "dockerfile": "Dockerfile", + "args": { + "VARIANT": "noble" + } + }, + + // Configure tool-specific properties. + // Note: Keep the list in alphabetical order. + "customizations": { + "vscode": { + "extensions": [ + "bierner.markdown-mermaid", + "DavidAnson.vscode-markdownlint", + "GitHub.copilot", + "GitHub.copilot-chat", + "GitHub.vscode-github-actions", + "ms-vscode.vscode-chat-customizations-evaluations", + "vscodevim.vim", + "vsls-contrib.codetour", + "xaver.clang-format" + ] + } + }, + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "ghcr.io/devcontainers-contrib/features/actionlint:1": {}, + "ghcr.io/devcontainers-contrib/features/node-asdf:0": {}, + "ghcr.io/devcontainers-extra/features/pipx-package:1": {}, + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/python:1": {}, + "ghcr.io/guiyomh/features/vim:0": {}, + "ghcr.io/jungaretti/features/make:1": {}, + "ghcr.io/jungaretti/features/ripgrep:1": {}, + "ghcr.io/prulloac/devcontainer-features/pre-commit:1": {}, + "ghcr.io/sliekens/devcontainer-features/opencode:1": {} + }, + + // Pre-create host directories so OpenCode can bind-mount persistent config and cache data into the container + "initializeCommand": "mkdir -p \"$HOME/.local/share/opencode\" \"$HOME/.config/opencode\"", + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "uname -a", + + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + // "image": "mcr.microsoft.com/devcontainers/base:jammy", + + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + // Note: Python dependencies can be added in the `requirements.txt` file. + "onCreateCommand": "sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ansible && ansible-playbook .devcontainer/provision.yml" } diff --git a/.devcontainer/provision.yml b/.devcontainer/provision.yml new file mode 100644 index 0000000..53212ed --- /dev/null +++ b/.devcontainer/provision.yml @@ -0,0 +1,51 @@ +--- +# Ansible playbook file. +# Usage: ansible-playbook .devcontainer/provision.yml +# Note: Keep keys and lists in alphabetical order. +- name: Provision dev container + hosts: localhost + connection: local + become: true + tasks: + - name: Load apt packages from file + ansible.builtin.set_fact: + apt_packages: >- + {{ lookup('file', playbook_dir + '/apt-packages.txt').splitlines() + | map('trim') | reject('equalto', '') | reject('match', '^#') | list }} + - name: Update apt cache + ansible.builtin.apt: + update_cache: true + - name: Install apt packages + ansible.builtin.apt: + name: '{{ apt_packages }}' + state: present + - name: Install ansible via pipx + ansible.builtin.command: pipx install --include-deps ansible + args: + creates: '{{ ansible_env.HOME }}/.local/bin/ansible' + become: false + - name: Inject ansible python requirements + ansible.builtin.command: + argv: + - pipx + - runpip + - ansible + - install + - -r + - '{{ playbook_dir }}/requirements-ansible.txt' + become: false + register: pipx_runpip + changed_when: "'Successfully installed' in (pipx_runpip.stdout + pipx_runpip.stderr)" + failed_when: pipx_runpip.rc != 0 + - name: Install python requirements + ansible.builtin.pip: + requirements: '{{ playbook_dir }}/requirements.txt' + extra_args: --break-system-packages --user + state: present + become: false + - name: Install pre-commit hooks + ansible.builtin.command: pre-commit install + args: + chdir: '{{ playbook_dir }}/..' + creates: .git/hooks/pre-commit + become: false diff --git a/.devcontainer/requirements-ansible.txt b/.devcontainer/requirements-ansible.txt new file mode 100644 index 0000000..9fe8088 --- /dev/null +++ b/.devcontainer/requirements-ansible.txt @@ -0,0 +1,4 @@ +# Python packages injected into the pipx-managed Ansible environment. +# Keep dependencies sorted alphabetically. +docker>=7.1 +requests==2.32.5 diff --git a/.devcontainer/requirements.txt b/.devcontainer/requirements.txt index 1fbb8b0..cfd4496 100644 --- a/.devcontainer/requirements.txt +++ b/.devcontainer/requirements.txt @@ -1,9 +1,14 @@ # Python's requirements # Usage: pip install -r requirements.txt +# Note: Keep dependencies sorted alphabetically. ansible ansible-lint +argcomplete>=1.9.4 docker>=7.1 jinja2-cli +molecule +molecule-docker pipenv pre-commit -requests==2.31.0 +requests==2.32.5 +uv diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 3e98d11..badf688 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -18,8 +18,8 @@ jobs: - uses: actions/setup-python@v5 - name: Installs shfmt run: > - sudo install /dev/stdin /usr/local/bin/shfmt - < <(curl -L "$SHFMT_URL") + curl -fsSL "$SHFMT_URL" | sudo tee /usr/local/bin/shfmt >/dev/null && + sudo chmod +x /usr/local/bin/shfmt env: # yamllint disable rule:line-length SHFMT_URL: https://github.com/mvdan/sh/releases/download/v3.1.1/shfmt_v3.1.1_linux_amd64 diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 0000000..176e8f8 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,67 @@ +--- +# GitHub Copilot cloud agent customization. +# +name: Copilot Setup Steps +env: + # Pin external repositories to specific SHAs for reproducibility. + COGNI_AI_AGENTS_REF: main + COGNI_AI_AGENT_INSTRUCTIONS_REF: main + COGNI_AI_AGENT_SKILLS_REF: main +# yamllint disable-line rule:truthy +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + - .devcontainer/requirements.txt + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + - .devcontainer/requirements.txt +jobs: + copilot-setup-steps: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + - name: Clone agents + run: | + git clone --branch "$COGNI_AI_AGENTS_REF" --depth=1 \ + https://github.com/Cogni-AI-OU/cogni-ai-agents \ + "$HOME/.copilot/agents" + - name: Clone instructions + run: | + git clone --branch "$COGNI_AI_AGENT_INSTRUCTIONS_REF" --depth=1 \ + https://github.com/Cogni-AI-OU/cogni-ai-agent-instructions \ + "$HOME/.copilot/instructions" + - name: Clone skills + run: | + git clone --branch "$COGNI_AI_AGENT_SKILLS_REF" --depth=1 \ + https://github.com/Cogni-AI-OU/cogni-ai-agent-skills \ + "$HOME/.copilot/skills" + - uses: actions/setup-python@v6.2.0 + with: + cache: >- + ${{ hashFiles('.devcontainer/requirements.txt', 'requirements.txt', + '**/pyproject.toml') != '' && 'pip' || '' }} + cache-dependency-path: | + .devcontainer/requirements.txt + requirements.txt + **/pyproject.toml + python-version: '3.12' + - name: Restore Python user site + id: python-user-site + uses: actions/cache@v5.0.5 + with: + path: ~/.local + key: ${{ runner.os }}-pip-${{ hashFiles('.devcontainer/requirements.txt') }} + - name: Install Python dependencies + run: | + if [ -f .devcontainer/requirements.txt ]; then + pip install -r .devcontainer/requirements.txt + elif [ -f requirements.txt ]; then + pip install -r requirements.txt + fi + - name: Ensure local bin on PATH + run: echo "$HOME/.local/bin" >> "$GITHUB_PATH" diff --git a/.github/workflows/devcontainer-ci.yml b/.github/workflows/devcontainer-ci.yml new file mode 100644 index 0000000..a1820e7 --- /dev/null +++ b/.github/workflows/devcontainer-ci.yml @@ -0,0 +1,151 @@ +--- +# Note: Keep keys and envs in alphabetical order. +name: Development Containers (CI) +# yamllint disable-line rule:truthy +on: + pull_request: + paths: + - .devcontainer/** + - .github/workflows/devcontainer-ci.yml + types: + - opened + - synchronize + - reopened + - ready_for_review + push: + branches: + - dev + - main + paths: + - .devcontainer/** + - .github/workflows/devcontainer-ci.yml + schedule: + - cron: 0 0 * * 1 # Run every Monday at 00:00 UTC + workflow_call: + # IMPORTANT: When calling this reusable workflow from another repository, + # you MUST grant 'packages: write' permission in the calling workflow. + # Example: + # jobs: + # devcontainer: + # uses: Cogni-AI-OU/.github/.github/workflows/devcontainer-ci.yml@main + # permissions: + # contents: read + # packages: write # Required for pushing to GitHub Container Registry + inputs: + required_commands: + description: Space-separated list of required command-line tools + required: false + type: string + default: '' + required_python_packages: + description: Space-separated list of required Python packages + required: false + type: string + default: '' +env: + # Keep the commands and packages in lexicographical order. + REQUIRED_COMMANDS: >- + ${{ inputs.required_commands != '' && inputs.required_commands || 'actionlint + ansible + docker + gh + make + node + npm + pip + pre-commit + python3 + rg' }} + REQUIRED_PYTHON_PACKAGES: >- + ${{ inputs.required_python_packages != '' && inputs.required_python_packages || 'ansible + ansible-lint + docker + molecule + pre-commit + uv' }} +permissions: + contents: read + packages: write +jobs: + devcontainer-build: + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + name: Build & Test + permissions: + contents: read + packages: write # Enables push to GHCR + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Login to GitHub Container Registry + uses: docker/login-action@v4.2.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Set image name + id: image + run: | + # Transform repository name to valid Docker tag format + # 1. Convert to lowercase (Docker tags must be lowercase) + # 2. Remove leading dots from path components (Docker components must start with alphanumeric) + REPO_NAME="${{ github.repository }}" + REPO_LOWER="${REPO_NAME,,}" + # Remove /. pattern to handle repos like "org/.github" -> "org/github" + SAFE_REPO_NAME="${REPO_LOWER//\/\./\/}" + echo "name=ghcr.io/${SAFE_REPO_NAME}/devcontainer" >> "$GITHUB_OUTPUT" + - name: Check cache image existence + id: cache_check + continue-on-error: true + run: | + IMAGE_NAME="${{ steps.image.outputs.name }}" + echo "Checking if cache image exists: ${IMAGE_NAME}:latest" + if docker pull "${IMAGE_NAME}:latest" 2>/dev/null; then + echo "✓ Cache image found: ${IMAGE_NAME}:latest" + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "⚠ Cache image not found: ${IMAGE_NAME}:latest" + echo "⚠ Build will proceed without cache (first build or image expired)" + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + - name: Build and test dev container + uses: devcontainers/ci@v0.3 + with: + imageName: ${{ steps.image.outputs.name }} + cacheFrom: ${{ steps.image.outputs.name }} + push: filter + refFilterForPush: refs/heads/main + runCmd: |- + echo "Testing devcontainer build..." + + # Check all required commands are installed + echo "Checking required command-line tools..." + for cmd in $REQUIRED_COMMANDS; do + if ! command -v "$cmd" &> /dev/null; then + echo "✗ $cmd is not installed" + exit 1 + fi + echo "✓ $cmd is installed" + done + + # Check all required Python packages are installed + echo "Checking required Python packages..." + for pkg in $REQUIRED_PYTHON_PACKAGES; do + if ! python3 -m pip show "$pkg" &> /dev/null; then + echo "✗ Required Python package '$pkg' is missing" + exit 1 + fi + echo "✓ $pkg is installed" + done + echo "✓ All required Python packages are installed" + + # Verify pre-commit can run (hooks may not be installed in CI container) + if pre-commit --version &> /dev/null; then + echo "✓ pre-commit is functional" + else + echo "✗ pre-commit is not functional" + exit 1 + fi + echo "✓ All devcontainer tests passed!" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 540ae69..95af748 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,15 +8,19 @@ repos: args: ["-c", ".yamllint", "-s"] - repo: https://github.com/ansible-community/ansible-lint - rev: v25.1.3 + rev: v6.22.2 hooks: - id: ansible-lint - files: ^ansible/ + language_version: python3 + exclude: \.github/workflows/.*\.lock\.yml$ + additional_dependencies: + - ansible-core>=2.15,<2.16 - repo: https://github.com/igorshubovych/markdownlint-cli.git - rev: v0.44.0 + rev: v0.46.0 hooks: - id: markdownlint + language_version: 22.14.0 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 @@ -27,7 +31,6 @@ repos: - id: check-docstring-first - id: check-executables-have-shebangs - id: check-merge-conflict - - id: check-json - id: check-yaml - id: end-of-file-fixer exclude: \.txt$ @@ -41,11 +44,7 @@ repos: - id: forbid-binary exclude: ^(lib|conf/servers)/.*$ - id: git-check # Configure in .gitattributes - - id: markdownlint # Configure in .mdlrc - - id: reek - # - id: require-ascii # - id: shellcheck - - id: shfmt - repo: https://github.com/rhysd/actionlint rev: v1.7.7 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2534a19 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "chat.tools.terminal.autoApprove": { + "pre-commit": true + } +}