diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5ae5361..03e4077 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,6 @@ "remoteUser": "runwhen", "updateRemoteUserUID": false, "overrideCommand": false, - "workspaceFolder": "/home/runwhen", "forwardPorts": [3000], "portsAttributes": { @@ -20,6 +19,7 @@ "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}" }, + "postCreateCommand": "chmod 755 /home/runwhen && mkdir -p /home/runwhen/.ssh && chmod 700 /home/runwhen/.ssh && touch /home/runwhen/.ssh/authorized_keys && chmod 600 /home/runwhen/.ssh/authorized_keys", "postStartCommand": "python -m http.server --bind 0.0.0.0 --directory /robot_logs 3000 &", "features": { @@ -63,7 +63,7 @@ } }, "codespaces": { - "openFiles": ["codecollection/README.md"] + "openFiles": ["README.md"] } } } diff --git a/.devcontainer/on-create.sh b/.devcontainer/on-create.sh deleted file mode 100755 index 9f64dbf..0000000 --- a/.devcontainer/on-create.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/bin/bash -# ====================================================================================== -# on-create.sh — Bootstrap a codecollection into the devtools environment -# -# Runs once when the devcontainer is created (onCreateCommand). -# Clones the target codecollection repo, installs its Python deps, and -# optionally checks out a PR branch for review. -# -# Environment variables (set via devcontainer.json or Codespaces secrets): -# -# CODECOLLECTION_REPO Git URL or GitHub shorthand (org/repo) of the -# codecollection to work on. -# Default: runwhen-contrib/rw-cli-codecollection -# -# CODECOLLECTION_BRANCH Branch to check out after clone. -# Default: main -# -# PR_NUMBER If set, fetch and check out the PR branch instead -# of CODECOLLECTION_BRANCH. Requires gh CLI auth. -# -# GITHUB_TOKEN Passed through for gh CLI auth (Codespaces injects -# this automatically). -# ====================================================================================== -set -euo pipefail - -RUNWHEN_HOME="/home/runwhen" -CODECOLLECTION_DIR="${RUNWHEN_HOME}/codecollection" -DEFAULT_REPO="runwhen-contrib/rw-cli-codecollection" - -REPO="${CODECOLLECTION_REPO:-$DEFAULT_REPO}" -BRANCH="${CODECOLLECTION_BRANCH:-main}" - -# Normalise shorthand "org/repo" → full HTTPS URL -if [[ "$REPO" != http* && "$REPO" != git@* ]]; then - REPO="https://github.com/${REPO}.git" -fi - -echo "=== CodeCollection DevTools Bootstrap ===" -echo " Repo: ${REPO}" -echo " Branch: ${BRANCH}" -echo " PR: ${PR_NUMBER:-none}" -echo "==========================================" - -# ------------------------------------------------------------------ -# 1. Clone the codecollection -# ------------------------------------------------------------------ -if [ -d "${CODECOLLECTION_DIR}/.git" ]; then - echo "Codecollection already cloned at ${CODECOLLECTION_DIR}, pulling latest..." - git -C "${CODECOLLECTION_DIR}" fetch --all --prune -else - # Remove placeholder dir if the image created one - rm -rf "${CODECOLLECTION_DIR}" - echo "Cloning ${REPO} → ${CODECOLLECTION_DIR} ..." - git clone --branch "${BRANCH}" "${REPO}" "${CODECOLLECTION_DIR}" -fi - -# ------------------------------------------------------------------ -# 2. Optionally check out a PR -# ------------------------------------------------------------------ -if [ -n "${PR_NUMBER:-}" ]; then - echo "Checking out PR #${PR_NUMBER}..." - cd "${CODECOLLECTION_DIR}" - if command -v gh &>/dev/null; then - gh pr checkout "${PR_NUMBER}" - else - echo "gh CLI not found — falling back to git fetch" - git fetch origin "pull/${PR_NUMBER}/head:pr-${PR_NUMBER}" - git checkout "pr-${PR_NUMBER}" - fi - echo "On branch: $(git branch --show-current)" -fi - -# ------------------------------------------------------------------ -# 3. Install codecollection Python dependencies -# ------------------------------------------------------------------ -if [ -f "${CODECOLLECTION_DIR}/requirements.txt" ]; then - echo "Installing codecollection requirements..." - pip install --user --no-cache-dir -r "${CODECOLLECTION_DIR}/requirements.txt" -fi - -# ------------------------------------------------------------------ -# 4. Install CodeBundle authoring skills as Cursor rules -# ------------------------------------------------------------------ -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -DEVTOOLS_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -SKILLS_SRC="${DEVTOOLS_ROOT}/skills" -RULES_DIR="${CODECOLLECTION_DIR}/.cursor/rules" - -if [ -d "${SKILLS_SRC}" ]; then - mkdir -p "${RULES_DIR}" - for skill in "${SKILLS_SRC}"/*.md; do - [ -f "$skill" ] || continue - base=$(basename "$skill" .md) - cp "$skill" "${RULES_DIR}/${base}.mdc" - echo " Installed skill: ${base}" - done - if [ ! -f "${RULES_DIR}/.gitignore" ]; then - printf '# Injected by codecollection-devtools -- do not commit\n*.mdc\n' > "${RULES_DIR}/.gitignore" - fi - echo "Skills installed to ${RULES_DIR}" -else - echo "Skills directory not found at ${SKILLS_SRC}, skipping." -fi - -# ------------------------------------------------------------------ -# 5. Ensure auth directory exists for credential mounts -# ------------------------------------------------------------------ -mkdir -p "${RUNWHEN_HOME}/auth" - -# ------------------------------------------------------------------ -# 6. Verify key tools are available -# ------------------------------------------------------------------ -echo "" -echo "--- Environment ready ---" -echo " ro: $(command -v ro && echo 'ok' || echo 'MISSING')" -echo " robot: $(command -v robot && echo 'ok' || echo 'MISSING')" -echo " kubectl: $(command -v kubectl && echo 'ok' || echo 'MISSING')" -echo " gh: $(command -v gh && echo 'ok' || echo 'MISSING')" -echo " python: $(python --version 2>&1)" -echo "" -echo "Codecollection bootstrapped at ${CODECOLLECTION_DIR}" -echo "Run 'cd codecollection/codebundles/ && ro' to test a codebundle." diff --git a/.gitignore b/.gitignore index c33a103..051548f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,8 @@ __pycache__ *robot_logs* .python-version .vscode/settings.json +# Generated by `task install-skills` (Cursor rules); do not commit +.cursor/rules/ node_modules/ package-lock.json package.json -/workspace/codecollection-devtools/rw-cli-codecollection -/workspace/codecollection-devtools/rw-generic-codecollection diff --git a/Dockerfile b/Dockerfile index 1635c1f..b1cf68c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,6 +48,22 @@ RUN set -eux; \ apt-get clean; \ rm -rf /var/lib/apt/lists/* +# docker-in-docker (devcontainer feature) sources /etc/os-release; "forky" is rejected. +# Align with bookworm apt pin above so Codespaces can use the CI-built image + dind. +RUN set -eux; \ + for f in /usr/lib/os-release /etc/os-release; do \ + [ -e "$f" ] || continue; \ + target="$f"; \ + [ -L "$f" ] && target=$(readlink -f "$f"); \ + grep -q '^ID=debian' "$target" || continue; \ + sed -i \ + -e 's/^VERSION_CODENAME=.*/VERSION_CODENAME=bookworm/' \ + -e 's/^VERSION_ID=.*/VERSION_ID="12"/' \ + -e 's/^VERSION=.*/VERSION="12 (bookworm)"/' \ + -e 's|^PRETTY_NAME=.*|PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"|' \ + "$target"; \ + done + RUN echo "runwhen ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers # Architecture detection for multi-arch tool installs @@ -160,9 +176,11 @@ RUN mkdir -p $ROBOT_LOG_DIR && \ COPY --chown=runwhen:0 .pylintrc.google LICENSE ro requirements.txt . COPY --chown=runwhen:0 .devcontainer/ .devcontainer/ -RUN mkdir -p auth && \ - chown -R runwhen:0 ${RUNWHEN_HOME}/.devcontainer ${RUNWHEN_HOME}/auth && \ - chmod -R 0775 ${RUNWHEN_HOME}/ro ${RUNWHEN_HOME}/auth ${RUNWHEN_HOME}/.devcontainer +RUN mkdir -p auth .ssh && \ + chown -R runwhen:0 ${RUNWHEN_HOME}/.devcontainer ${RUNWHEN_HOME}/auth ${RUNWHEN_HOME}/.ssh && \ + chmod -R 0775 ${RUNWHEN_HOME}/ro ${RUNWHEN_HOME}/auth ${RUNWHEN_HOME}/.devcontainer && \ + chmod 755 ${RUNWHEN_HOME} && \ + chmod 700 ${RUNWHEN_HOME}/.ssh USER runwhen ENV USER="runwhen" diff --git a/README.md b/README.md index f0ae905..00c27a7 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ generation rules, SLI patterns, and test infrastructure conventions. | `test-infra-azure-devops.md` | DevOps projects, pipelines, agent pools via Terraform | | `test-infra-cloud.md` | Shared conventions across all cloud platforms | -Skills are copied to `{codecollection}/.cursor/rules/*.mdc` at setup time. A +Skills are copied to `.cursor/rules/*.mdc` (the workspace root) at setup time. A `.gitignore` is placed in that directory to prevent accidental commits. To re-install after an update, run: @@ -197,7 +197,7 @@ Base packages installed in the image (from `requirements.txt`): - `rw-cli-keywords` (includes `rw-core-keywords` — handles `RW_MODE=dev` for local development) - `jmespath`, `python-dateutil`, `thefuzz`, `jinja2` -Each codecollection's `requirements.txt` is installed at bootstrap time by `on-create.sh`. +Each codecollection's `requirements.txt` is installed at bootstrap time by `task setup`. --- @@ -206,8 +206,7 @@ Each codecollection's `requirements.txt` is installed at bootstrap time by `on-c ``` codecollection-devtools/ ├── .devcontainer/ -│ ├── devcontainer.json # devcontainer config (pulls pre-built image) -│ └── on-create.sh # bootstrap script (called by Taskfile) +│ └── devcontainer.json # devcontainer config (pulls pre-built image) ├── .github/ │ └── workflows/ │ ├── build-push.yaml # CI: multi-arch build → GHCR + GCP Artifact Registry @@ -246,16 +245,23 @@ All image builds happen in **GitHub Actions** — never locally: ``` devcontainer opens → pulls pre-built image from GHCR + → workspace root is /workspaces/codecollection-devtools/ (the repo mount) → starts log HTTP server on port 3000 → user runs: task setup REPO=org/repo PR=123 1. clones repo into /home/runwhen/codecollection/ - 2. checks out PR branch (if PR set) - 3. pip installs codecollection's requirements.txt - 4. installs skills/ as .cursor/rules/*.mdc - 5. verifies tools (ro, robot, kubectl, gh, python) - → ready to develop + 2. symlinks ./codecollection → /home/runwhen/codecollection + 3. checks out PR branch (if PR set) + 4. pip installs codecollection's requirements.txt + 5. installs skills/ as .cursor/rules/*.mdc (workspace root) + 6. verifies tools (ro, robot, kubectl, gh, python) + → ready: cd codecollection/codebundles/ && ro ``` +> **Two repos, one tree.** The workspace root is the devtools repo. The `codecollection/` +> directory is a symlink to a separate git clone. `git` commands inside `codecollection/` +> operate on the codecollection repo — not devtools. The `.gitignore` prevents the +> symlink from being tracked. + --- ## For codecollection authors diff --git a/Taskfile.yml b/Taskfile.yml index f13456a..c5654f9 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -54,6 +54,15 @@ tasks: echo "Cloning {{.REPO_URL}} → {{.CODECOLLECTION_DIR}} ..." git clone --branch "{{.BRANCH}}" "{{.REPO_URL}}" "{{.CODECOLLECTION_DIR}}" fi + - | + LINK="{{.TASKFILE_DIR}}/codecollection" + if [ "$(readlink -f "$LINK" 2>/dev/null)" = "$(readlink -f "{{.CODECOLLECTION_DIR}}")" ]; then + : + else + rm -f "$LINK" + ln -s "{{.CODECOLLECTION_DIR}}" "$LINK" + echo "Symlinked $LINK → {{.CODECOLLECTION_DIR}}" + fi checkout-pr: desc: Check out a PR branch @@ -84,22 +93,20 @@ tasks: desc: Install CodeBundle authoring skills as Cursor rules cmds: - | - RULES_DIR="{{.CODECOLLECTION_DIR}}/.cursor/rules" if [ ! -d "{{.SKILLS_SRC}}" ]; then echo "Skills directory not found at {{.SKILLS_SRC}}, skipping." exit 0 fi + RULES_DIR="{{.TASKFILE_DIR}}/.cursor/rules" mkdir -p "$RULES_DIR" for skill in "{{.SKILLS_SRC}}"/*.md; do [ -f "$skill" ] || continue base=$(basename "$skill" .md) - target="$RULES_DIR/${base}.mdc" - cp "$skill" "$target" - echo " Installed skill: ${base}" + cp "$skill" "$RULES_DIR/${base}.mdc" + echo " Installed: ${base}" done if [ ! -f "$RULES_DIR/.gitignore" ]; then - echo "# Injected by codecollection-devtools -- do not commit" > "$RULES_DIR/.gitignore" - echo "*.mdc" >> "$RULES_DIR/.gitignore" + printf '# Injected by codecollection-devtools -- do not commit\n*.mdc\n' > "$RULES_DIR/.gitignore" fi echo "Skills installed to $RULES_DIR" @@ -114,12 +121,17 @@ tasks: - 'printf " %-12s %s\n" "gh:" "$(command -v gh 2>/dev/null && echo ok || echo MISSING)"' - 'printf " %-12s %s\n" "python:" "$(python --version 2>&1)"' - echo "" - - echo "Codecollection at {{.CODECOLLECTION_DIR}}" - - echo "Run 'cd codecollection/codebundles/ && ro' to test a codebundle." + - echo "Codecollection at {{.CODECOLLECTION_DIR}} (symlinked from ./codecollection)" + - echo "" + - echo "Your work happens in the codecollection (its own git repo):" + - echo " cd codecollection/codebundles/ && ro" + - echo "" + - echo "Commits in ./codecollection go to the codecollection repo, not devtools." clean: desc: Remove the cloned codecollection (start fresh) prompt: This will delete {{.CODECOLLECTION_DIR}}. Continue? cmds: + - rm -f "{{.TASKFILE_DIR}}/codecollection" - rm -rf "{{.CODECOLLECTION_DIR}}" - echo "Cleaned. Run 'task setup REPO=... ' to start again." diff --git a/docs/cursor-remote-devcontainer.md b/docs/cursor-remote-devcontainer.md index 13d5314..c16a619 100644 --- a/docs/cursor-remote-devcontainer.md +++ b/docs/cursor-remote-devcontainer.md @@ -75,20 +75,14 @@ sed -i.bak \ ~/.ssh/codespaces ``` -In Cursor: **Remote-SSH: Connect to Host…** → select that host → **Open Folder** → **`/home/runwhen`**. +In Cursor: **Remote-SSH: Connect to Host…** → select that host → **Open Folder** → **`/workspaces/codecollection-devtools`** (the devcontainer workspace root). After `task setup`, the `codecollection/` symlink gives you direct access to codebundles. --- ## 5. Troubleshooting - Run **`gh codespace ssh -c YOUR_CODESPACE_NAME --`** again if the connection or auth state seems off. -- For **`authorized_keys`** permission errors inside the codespace: - - ```bash - sudo chmod 755 /home/runwhen - sudo chmod 700 /home/runwhen/.ssh - sudo chmod 600 /home/runwhen/.ssh/authorized_keys - ``` +- SSH permission errors should not occur — the image and `postCreateCommand` set `/home/runwhen` (755), `.ssh` (700), and `authorized_keys` (600) automatically. ---