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
48 changes: 48 additions & 0 deletions .github/workflows/check-image-tags.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
*.DS_Store
Thumbs.db
node_modules
node_modules
# Local clone of devcontainers/images used by build/check-image-tags.ts
/images/
132 changes: 132 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# 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/<template-id>/` and is mirrored by tests in `test/<template-id>/`.

A template is made of:

- `src/<id>/devcontainer-template.json` — template metadata and user-selectable `options`.
- `src/<id>/.devcontainer/` — the `devcontainer.json` (and optional `Dockerfile` /
`docker-compose.yml`) that the template generates.
- `src/<id>/NOTES.md`, `src/<id>/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 <path-to-images-repo>`.
- `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 <files>` 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`. 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
convention (cross-check the image's `src/<image>/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`).
- **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
`.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`.
138 changes: 138 additions & 0 deletions build/report-drift-issue.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env bash
#
# 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 <images>`
# (ANSI colour codes already stripped).
# Usage: build/report-drift-issue.sh <report-file>
# 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 <report-file>}"
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 <images-repo>\` 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 "<details><summary>Show $(printf '%s\n' "$unused" | wc -l | tr -d ' ') unused tags</summary>"
echo
echo '```'
echo "$unused"
echo '```'
echo
echo "</details>"
echo
fi

echo "<details><summary>Full comparison report</summary>"
echo
echo '```'
cat "$REPORT_FILE"
echo '```'
echo
echo "</details>"
echo
echo "---"
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 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 "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 ----------------------------------------------
# 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."
Loading