From 061683fcb3707afb32eb405ad176f89d65baea35 Mon Sep 17 00:00:00 2001 From: Kaniska Date: Tue, 30 Jun 2026 09:13:48 +0000 Subject: [PATCH 1/2] Automated PR creation for templates updates. --- .github/workflows/check-image-tags.yaml | 48 +++++++++ .gitignore | 4 +- AGENTS.md | 119 ++++++++++++++++++++ build/report-drift-issue.sh | 137 ++++++++++++++++++++++++ 4 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 AGENTS.md create mode 100755 build/report-drift-issue.sh diff --git a/.github/workflows/check-image-tags.yaml b/.github/workflows/check-image-tags.yaml index f5214a87..4a1d156b 100644 --- a/.github/workflows/check-image-tags.yaml +++ b/.github/workflows/check-image-tags.yaml @@ -54,3 +54,51 @@ jobs: if ! npx tsx build/check-image-tags.ts images; then echo "::warning::Image tag check against latest images failed - upcoming release may break templates" fi + + report-drift-issue: + name: Report Drift and Assign to Copilot + # Only run for scheduled syncs and manual runs - never on PRs/pushes, so we don't + # open issues for in-flight changes. + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - uses: actions/checkout@v5 + + - uses: actions/checkout@v5 + with: + repository: devcontainers/images + fetch-depth: 0 + path: images + + - name: Check out last release tag + run: | + cd images + tag=$(git describe --tags --abbrev=0) + echo "Checking out tag: $tag" + git checkout "$tag" + + - name: Run comparison and capture report + id: compare + run: | + set +e + npx tsx build/check-image-tags.ts images > report.txt 2>&1 + status=$? + # Strip ANSI colour codes so the report is readable inside an issue. + sed -i -E 's/\x1B\[[0-9;]*m//g' report.txt + cat report.txt + if grep -qE '^[[:space:]]*(MISSING|UNUSED)' report.txt; then + echo "has_drift=true" >> "$GITHUB_OUTPUT" + else + echo "has_drift=false" >> "$GITHUB_OUTPUT" + fi + + - name: Create or refresh tracking issue and assign to Copilot + if: steps.compare.outputs.has_drift == 'true' + env: + # PAT is used (rather than GITHUB_TOKEN) so the Copilot coding agent is reliably + # triggered by the assignment. Requires `issues: write` and Copilot enabled. + GH_TOKEN: ${{ secrets.PAT }} + run: bash build/report-drift-issue.sh report.txt diff --git a/.gitignore b/.gitignore index 1dc59b5a..d07e4db5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.DS_Store Thumbs.db -node_modules \ No newline at end of file +node_modules +# Local clone of devcontainers/images used by build/check-image-tags.ts +/images/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..d2c42fca --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,119 @@ +# AGENTS.md + +Guidance for AI coding agents (including the GitHub Copilot coding agent) working in +this repository. + +## Repository overview + +This repo contains the official [Dev Container Templates](https://containers.dev/templates). +Each template lives in `src//` and is mirrored by tests in `test//`. + +A template is made of: + +- `src//devcontainer-template.json` — template metadata and user-selectable `options`. +- `src//.devcontainer/` — the `devcontainer.json` (and optional `Dockerfile` / + `docker-compose.yml`) that the template generates. +- `src//NOTES.md`, `src//README.md` — docs (README is auto-generated, do not + hand-edit; see below). + +Build/validation helpers live in `build/`: + +- `build/check-image-tags.ts` — compares the image tags referenced by templates against + the tags actually published by [devcontainers/images](https://github.com/devcontainers/images). + Run: `npx tsx build/check-image-tags.ts `. +- `build/list-template-images.ts` — lists every fully-qualified image a template produces. +- `build/increment-patch.sh` — bumps the patch version of every `devcontainer-template.json`. + +## General conventions + +- Formatting is enforced by Prettier using `.prettierrc` (4 spaces, no tabs, single + quotes in JS/TS). Run `npx prettier --write ` after editing JSON/TS. +- Do **not** hand-edit `src/*/README.md` — they are regenerated by the + `Update Documentation` workflow. +- Every change to a template **must** bump the `version` field in that template's + `devcontainer-template.json` (semantic versioning; a variant add/remove is a patch bump). +- Keep changes minimal and scoped to the templates that actually need updating. + +## How templates reference container images + +Templates pin a base image and expose its variants through a single string option +(usually named `imageVariant`). The image reference contains a placeholder that is +substituted with the chosen option value: + +```jsonc +// src/java/.devcontainer/devcontainer.json +"image": "mcr.microsoft.com/devcontainers/java:3-${templateOption:imageVariant}" +``` + +```jsonc +// src/java/devcontainer-template.json +"options": { + "imageVariant": { + "type": "string", + "proposals": ["25-trixie", "21-trixie", "...", "8-bookworm"], + "default": "25-trixie" + } +} +``` + +So the concrete image tag is `prefix` + `option value`, e.g. +`java:3-` + `25-trixie` = `java:3-25-trixie`. The `proposals` array is a **curated** +subset of `{version}-{os}` variants — it intentionally does **not** list every tag the +images repo publishes (floating tags such as `java:3-25`, OS-only tags such as +`java:3-trixie`, JDK/`-jdk` aliases, etc. are deliberately omitted). + +## Keeping templates in sync with devcontainers/images + +When [devcontainers/images](https://github.com/devcontainers/images) adds or removes an +image variant, the affected templates must be updated. The scheduled +"Compare Templates against Images" workflow opens an issue (assigned to the coding agent) +containing the output of `build/check-image-tags.ts`, which classifies tags as: + +- **MISSING** — referenced by a template but **no longer published** by images. + → Action: **remove** that variant from the matching template's + `options.imageVariant.proposals`. Authoritative, low-noise signal — act on every one. +- **UNUSED** — published by images but **not referenced** by any template. + → Mostly intentional (aliases, floating/OS-only tags). **Only add** an entry when it is + a genuinely new `{version}-{os}` variant that matches the template's existing naming + convention (cross-check the image's `src//manifest.json` `variants` array in the + images repo). Ignore floating/alias tags. + +### Editing rules + +1. **Identify the template** from the tag's image name (the part before `:`). Map the tag + back to a template by finding the `src/*/.devcontainer/*` file whose image reference + shares that prefix. Some templates map to several images (e.g. `php` and `php-mariadb`). +2. **Removals (MISSING):** delete the obsolete value from `proposals`. If `default` equals + a removed value, set `default` to the new newest variant (see ordering below). +3. **Additions (UNUSED, only genuine new variants):** insert the new value into `proposals` + in the correct position (see ordering below). + - If **every** variant for an image is reported MISSING, the image's pinned major in the + `.devcontainer` image reference has changed (e.g. `php:3-` → `php:4-`). Update that + prefix in the template's `devcontainer.json` / `Dockerfile` as well, then re-derive + the `proposals`. +4. **Proposals ordering convention** (match the existing files exactly): + - Group by OS, newest OS first. Observed priority: `trixie` → `bookworm` → `bullseye` + (Debian); for OS-named variants, Debian newest→oldest then Ubuntu newest→oldest + (e.g. `debian13`, `debian12`, `ubuntu24.04`, `ubuntu22.04`). + - Within an OS group, list versions **descending** (newest first). + - A floating major (e.g. Python's `3-trixie`) comes first within its OS group. +5. **Default convention:** the newest **concrete** version on the newest OS + (e.g. `25-trixie`, `1.26-trixie`; Python uses the concrete `3.14-trixie`, not the + floating `3-trixie`). +6. **Do not touch options that are not image variants** (e.g. cpp's + `reinstallCmakeVersionFromSource`, boolean feature toggles). +7. **Bump `version`** (patch) in each edited `devcontainer-template.json`. + +### Validating your change + +Run the checker against the images repo and confirm there are **no remaining MISSING tags** +for the templates you touched: + +```bash +# Clone the images repo somewhere, then: +npx tsx build/check-image-tags.ts ../images +``` + +A successful sync produces **zero MISSING** tags. Remaining UNUSED tags are expected and +acceptable (they are mostly intentional aliases). Finish by running +`npx prettier --write src/**/devcontainer-template.json`. diff --git a/build/report-drift-issue.sh b/build/report-drift-issue.sh new file mode 100755 index 00000000..0f67d8e2 --- /dev/null +++ b/build/report-drift-issue.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# +# Creates (or refreshes) a single tracking issue describing the drift between the +# templates in this repo and the images published by devcontainers/images, then assigns +# it to the GitHub Copilot coding agent so it can prepare the fix. +# +# Input: a plain-text report produced by `npx tsx build/check-image-tags.ts ` +# (ANSI colour codes already stripped). +# Usage: build/report-drift-issue.sh +# Env: GH_TOKEN / GITHUB_TOKEN - token with `issues: write` +# GITHUB_REPOSITORY - owner/name (defaults to devcontainers/templates) +# +set -euo pipefail + +REPORT_FILE="${1:?Usage: report-drift-issue.sh }" +REPO="${GITHUB_REPOSITORY:-devcontainers/templates}" +OWNER="${REPO%%/*}" +NAME="${REPO##*/}" +LABEL="automated-image-sync" +TITLE="Sync template image variants with devcontainers/images" +# Login of the Copilot coding agent actor (shows up as "Copilot" in the UI). +COPILOT_LOGIN="copilot-swe-agent" + +# --- Extract the actionable signals from the report --------------------------------- +missing="$(grep -E '^[[:space:]]*MISSING' "$REPORT_FILE" | sed -E 's/^[[:space:]]*MISSING[[:space:]]+//' | sort -u || true)" +unused="$(grep -E '^[[:space:]]*UNUSED' "$REPORT_FILE" | sed -E 's/^[[:space:]]*UNUSED[[:space:]]+//' | sort -u || true)" + +if [ -z "$missing" ] && [ -z "$unused" ]; then + echo "No drift detected; nothing to do." + exit 0 +fi + +# --- Build the issue body ------------------------------------------------------------ +body_file="$(mktemp)" +{ + echo "## Templates ↔ images drift detected" + echo + echo "The scheduled **Compare Templates against Images** workflow detected differences" + echo "between the image tags referenced by templates in this repo and the tags published" + echo "by [devcontainers/images](https://github.com/devcontainers/images)." + echo + echo "Please follow the rules in [AGENTS.md](https://github.com/${REPO}/blob/main/AGENTS.md)" + echo "(section *“Keeping templates in sync with devcontainers/images”*) to update the" + echo "affected templates, bump each edited template's \`version\`, and validate with" + echo "\`npx tsx build/check-image-tags.ts \` until there are **no MISSING tags**." + echo + + if [ -n "$missing" ]; then + echo "### ❌ MISSING — referenced by templates but no longer published" + echo "Remove these variants from the matching template's \`options.imageVariant.proposals\`" + echo "(update \`default\` if it pointed at one of them)." + echo + echo '```' + echo "$missing" + echo '```' + echo + fi + + if [ -n "$unused" ]; then + echo "### ⚠️ UNUSED — published but not referenced by any template" + echo "Most of these are **intentional** (floating tags, OS-only tags, aliases)." + echo "Only add an entry if it is a genuinely new \`{version}-{os}\` variant that matches a" + echo "template's existing convention (cross-check the image's \`manifest.json\` \`variants\`)." + echo + echo "
Show $(printf '%s\n' "$unused" | wc -l | tr -d ' ') unused tags" + echo + echo '```' + echo "$unused" + echo '```' + echo + echo "
" + echo + fi + + echo "
Full comparison report" + echo + echo '```' + cat "$REPORT_FILE" + echo '```' + echo + echo "
" + echo + echo "---" + echo "_Generated automatically by \`.github/workflows/check-image-tags.yaml\`. This issue is" + echo "refreshed on each scheduled run until the drift is resolved._" +} >"$body_file" + +# --- Ensure the tracking label exists ------------------------------------------------ +gh label create "$LABEL" --repo "$REPO" \ + --color "1d76db" --description "Automated templates/images variant sync" 2>/dev/null || true + +# --- Create or update a single open tracking issue ----------------------------------- +existing="$(gh issue list --repo "$REPO" --state open --label "$LABEL" \ + --json number --jq '.[0].number // empty' || true)" + +if [ -n "$existing" ]; then + echo "Refreshing existing issue #${existing}" + gh issue edit "$existing" --repo "$REPO" --body-file "$body_file" >/dev/null + issue_number="$existing" +else + echo "Creating new tracking issue" + issue_url="$(gh issue create --repo "$REPO" --title "$TITLE" \ + --label "$LABEL" --body-file "$body_file")" + issue_number="${issue_url##*/}" +fi +echo "Issue #${issue_number}: https://github.com/${REPO}/issues/${issue_number}" + +# --- Assign to the Copilot coding agent ---------------------------------------------- +# The agent must be enabled for the repo; it then appears as an assignable actor. +bot_id="$(gh api graphql -f owner="$OWNER" -f name="$NAME" -f query=' + query($owner:String!, $name:String!) { + repository(owner:$owner, name:$name) { + suggestedActors(capabilities:[CAN_BE_ASSIGNED], first:100) { + nodes { login __typename ... on Bot { id } ... on User { id } } + } + } + }' --jq ".data.repository.suggestedActors.nodes[] | select(.login==\"${COPILOT_LOGIN}\") | .id" || true)" + +if [ -z "$bot_id" ]; then + echo "::warning::Copilot coding agent ('${COPILOT_LOGIN}') is not assignable in ${REPO}. " \ + "Enable the Copilot coding agent for the repository. Issue left unassigned." + exit 0 +fi + +issue_id="$(gh api graphql -f owner="$OWNER" -f name="$NAME" -F number="$issue_number" -f query=' + query($owner:String!, $name:String!, $number:Int!) { + repository(owner:$owner, name:$name) { issue(number:$number) { id } } + }' --jq '.data.repository.issue.id')" + +gh api graphql -f assignableId="$issue_id" -f actorId="$bot_id" -f query=' + mutation($assignableId:ID!, $actorId:ID!) { + replaceActorsForAssignable(input:{assignableId:$assignableId, actorIds:[$actorId]}) { + assignable { ... on Issue { number assignees(first:5){nodes{login}} } } + } + }' >/dev/null + +echo "Assigned issue #${issue_number} to the Copilot coding agent." From 95ce17f5cd9d704c39fc4b4b5d5a651fb5cddfff Mon Sep 17 00:00:00 2001 From: Kaniska Date: Mon, 6 Jul 2026 16:02:51 +0000 Subject: [PATCH 2/2] Modification of the issue creation logic and updating agent instructions. --- AGENTS.md | 19 ++++++++++++++++--- build/report-drift-issue.sh | 29 +++++++++++++++-------------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d2c42fca..5649fa11 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,7 +71,9 @@ containing the output of `build/check-image-tags.ts`, which classifies tags as: - **MISSING** — referenced by a template but **no longer published** by images. → Action: **remove** that variant from the matching template's - `options.imageVariant.proposals`. Authoritative, low-noise signal — act on every one. + `options.imageVariant.proposals`. Low-noise signal, but see the caveat below: a tag can be + published by a **differently-named image directory**, so confirm it is genuinely gone from + the images repo before removing it. - **UNUSED** — published by images but **not referenced** by any template. → Mostly intentional (aliases, floating/OS-only tags). **Only add** an entry when it is a genuinely new `{version}-{os}` variant that matches the template's existing naming @@ -83,8 +85,19 @@ containing the output of `build/check-image-tags.ts`, which classifies tags as: 1. **Identify the template** from the tag's image name (the part before `:`). Map the tag back to a template by finding the `src/*/.devcontainer/*` file whose image reference shares that prefix. Some templates map to several images (e.g. `php` and `php-mariadb`). -2. **Removals (MISSING):** delete the obsolete value from `proposals`. If `default` equals - a removed value, set `default` to the new newest variant (see ordering below). + - **A single image tag may be published from more than one image directory.** The image + name (before `:`) does **not** always match the images-repo directory that publishes + the tag. Notably, the `java` template's `8-trixie` / `8-bookworm` variants produce + `java:3-8-trixie` / `java:3-8-bookworm`, which are published by the **separate + `src/java-8` image directory** — not by `src/java`. Do **not** remove these variants + just because they are absent from `src/java/manifest.json`; verify against + `src/java-8/manifest.json` first. The same applies to `java-postgres`, which builds on + the same `java` image. +2. **Removals (MISSING):** delete the obsolete value from `proposals` **only after** you have + confirmed the tag is not published by any image directory (grep every `src/*/manifest.json` + in the images repo for the tag, not just the one whose name matches the image prefix). If + `default` equals a removed value, set `default` to the new newest variant (see ordering + below). 3. **Additions (UNUSED, only genuine new variants):** insert the new value into `proposals` in the correct position (see ordering below). - If **every** variant for an image is reported MISSING, the image's pinned major in the diff --git a/build/report-drift-issue.sh b/build/report-drift-issue.sh index 0f67d8e2..6cd54ce6 100755 --- a/build/report-drift-issue.sh +++ b/build/report-drift-issue.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash # -# Creates (or refreshes) a single tracking issue describing the drift between the -# templates in this repo and the images published by devcontainers/images, then assigns -# it to the GitHub Copilot coding agent so it can prepare the fix. +# Creates a single tracking issue describing the drift between the templates in this repo +# and the images published by devcontainers/images, then assigns it to the GitHub Copilot +# coding agent so it can prepare the fix. A new issue is only created when a similar +# tracking issue isn't already open. # # Input: a plain-text report produced by `npx tsx build/check-image-tags.ts ` # (ANSI colour codes already stripped). @@ -81,28 +82,28 @@ body_file="$(mktemp)" echo "" echo echo "---" - echo "_Generated automatically by \`.github/workflows/check-image-tags.yaml\`. This issue is" - echo "refreshed on each scheduled run until the drift is resolved._" + echo "_Generated automatically by \`.github/workflows/check-image-tags.yaml\`. A new issue is" + echo "opened only when no similar tracking issue is already open._" } >"$body_file" # --- Ensure the tracking label exists ------------------------------------------------ gh label create "$LABEL" --repo "$REPO" \ --color "1d76db" --description "Automated templates/images variant sync" 2>/dev/null || true -# --- Create or update a single open tracking issue ----------------------------------- +# --- Create a single open tracking issue (only if one isn't already open) ------------ existing="$(gh issue list --repo "$REPO" --state open --label "$LABEL" \ --json number --jq '.[0].number // empty' || true)" if [ -n "$existing" ]; then - echo "Refreshing existing issue #${existing}" - gh issue edit "$existing" --repo "$REPO" --body-file "$body_file" >/dev/null - issue_number="$existing" -else - echo "Creating new tracking issue" - issue_url="$(gh issue create --repo "$REPO" --title "$TITLE" \ - --label "$LABEL" --body-file "$body_file")" - issue_number="${issue_url##*/}" + echo "An open tracking issue already exists (#${existing}); nothing to do." + echo "Issue #${existing}: https://github.com/${REPO}/issues/${existing}" + exit 0 fi + +echo "Creating new tracking issue" +issue_url="$(gh issue create --repo "$REPO" --title "$TITLE" \ + --label "$LABEL" --body-file "$body_file")" +issue_number="${issue_url##*/}" echo "Issue #${issue_number}: https://github.com/${REPO}/issues/${issue_number}" # --- Assign to the Copilot coding agent ----------------------------------------------