From 244b63921e7e87bdd2b3211e406c59a4d47d2619 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Oct 2026 21:29:21 +0000 Subject: [PATCH] chore(template-sync): sync to v1.11.6 --- .devcontainer/README.md | 21 +++++++++++++++++++-- .devcontainer/compose.yml | 14 +++++++++++--- .github/template-sync-state.json | 2 +- .github/workflows/README.md | 27 ++++++++++++++++++++------- .github/workflows/release.yml | 15 +++++++++++++-- docs/TEMPLATE.md | 21 +++++++++++++++++++-- scripts/develop.sh | 10 +++++++++- 7 files changed, 92 insertions(+), 18 deletions(-) diff --git a/.devcontainer/README.md b/.devcontainer/README.md index f0f7180..92e85e9 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -16,7 +16,8 @@ It also declares the optional `github_token` build secret (from the `GITHUB_TOKEN` environment variable, which CI exports) that authenticates `../scripts/develop.sh`'s downloads; unset locally, it is - simply absent. + simply absent. Its `environment:` sets `CLAUDE_CONFIG_DIR` to the + `claude-config` volume's mount path (see "Do"). - `compose.instance.yml` — instance-owned stub (`ignore` tier, never touched by sync): an instance's `include:` list and extra `services.myapp` settings. See `docs/TEMPLATE.md`'s "Instance extension points". @@ -155,6 +156,21 @@ need the window reconnected, not a fresh image. not by Renovate: Renovate can't regenerate this file, so `../.github/renovate.json` turns its devcontainer feature updates off rather than let them merge with a stale lockfile. +- Change `CLAUDE_CONFIG_DIR` (`compose.yml`'s `services.myapp` + `environment:`), `claude-config`'s mount path and the + `/home/vscode/.claude` paths in `../scripts/develop.sh` together. + develop.sh creates that directory owned by `vscode` at mode 0700, so a + fresh volume starts out writable by Claude Code. The + variable points Claude Code at the volume for all its state. Without + it, Claude Code keeps `.claude.json` (account, MCP servers, + per-project trust) at `~/.claude.json`, outside the volume, so a + rebuild loses it and first-launch setup runs again. See Claude Code's + [dev container docs](https://code.claude.com/docs/en/devcontainer#persist-authentication-and-settings-across-rebuilds). + The variable, or a new mount path, takes effect only on a full + rebuild, and that first rebuild still starts without `.claude.json`. + To keep it, quit Claude Code and run + `cp ~/.claude.json ~/.claude/.claude.json` (into the volume) as the + last step before rebuilding, or sign in once more afterwards. ## Don't @@ -162,7 +178,8 @@ need the window reconnected, not a fresh image. else you don't want a host antivirus/DLP tool scanning or syncing — it lives purely in the container's writable layer instead. (`claude-config` and `gh-config` are deliberate, narrow exceptions — small login and - session state; see `compose.yml`'s comments on those volumes.) + session state, including Claude Code's `.claude.json`; see + `compose.yml`'s comments on those volumes.) - Add a `ports:` mapping or a `networks:` block anywhere here. - Pin a feature or image version as `latest` — pin the exact version so Renovate can bump it deliberately. diff --git a/.devcontainer/compose.yml b/.devcontainer/compose.yml index fa6ec40..4c66bb4 100644 --- a/.devcontainer/compose.yml +++ b/.devcontainer/compose.yml @@ -30,6 +30,13 @@ services: # adds one. - claude-config:/home/vscode/.claude - gh-config:/home/vscode/.config/gh + environment: + # Keeps all Claude Code state, .claude.json included, in the + # claude-config volume. Must match its mount path above and the + # paths in scripts/develop.sh; see .devcontainer/README.md's "Do". + # Set here rather than in devcontainer.json's remoteEnv so every + # process in the container sees it, not just VS Code's. + CLAUDE_CONFIG_DIR: /home/vscode/.claude command: sleep infinity # No ports: mapping and no networks: block — see .devcontainer/ # README.md's "Don't". An instance that needs host access adds @@ -39,9 +46,10 @@ services: volumes: # Persists Claude Code CLI session/credential state across container - # rebuilds. A small amount of session-state data, not a language - # runtime's dependency tree, so the DLP/malware-scanning concern above - # doesn't apply the same way here. + # rebuilds, .claude.json included (see CLAUDE_CONFIG_DIR above). A + # small amount of session-state data, not a language runtime's + # dependency tree, so the DLP/malware-scanning concern above doesn't + # apply the same way here. claude-config: # Persists the GitHub CLI's login (`gh auth login`) across rebuilds, for # the same reason. diff --git a/.github/template-sync-state.json b/.github/template-sync-state.json index 82b7734..28443bc 100644 --- a/.github/template-sync-state.json +++ b/.github/template-sync-state.json @@ -1,4 +1,4 @@ { "template_repo": "craftainer/template-base", - "synced_tag": "v1.11.4" + "synced_tag": "v1.11.6" } diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 4d5ac62..7053f91 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -256,9 +256,15 @@ workflow works with no registry configured at all. When it is set: - `OCI_IMAGE_NAME` (variable, optional) — the image path within the registry, e.g. `myorg/template-fastapi`. Defaults to the repository's own `owner/repo` (lowercased) if unset. -- `OCI_REGISTRY_USERNAME` / `OCI_REGISTRY_PASSWORD` (secrets, - required whenever `OCI_REGISTRY` is set) — credentials for - `docker/login-action`. +- `OCI_REGISTRY_USERNAME` / `OCI_REGISTRY_PASSWORD` (secrets) — + credentials for `docker/login-action`. Required whenever + `OCI_REGISTRY` is set, except to push to the repository's own GHCR + packages (`ghcr.io//...`): the `build` and `release` + jobs' `GITHUB_TOKEN` has `packages: write`, so the instance's + `publish` and `finalize` can log in with `GITHUB_ACTOR`/`GITHUB_TOKEN` instead (see + `../../docs/TEMPLATE.md`'s "Release: a Makefile contract"). The + contract asks an instance's `publish` to prefer the secrets when they + are set; they remain the way to reach any other registry. ## Image labels @@ -286,7 +292,10 @@ cleanly when the registry isn't configured. - `OCI_REGISTRY` (variable) — registry host (e.g. `ghcr.io`). - `OCI_IMAGE_NAME` (variable, optional) — image path within the registry. - `OCI_REGISTRY_USERNAME` / `OCI_REGISTRY_PASSWORD` (secrets) — - registry credentials. + registry credentials. Optional for the repository's own GHCR + packages, which the `build` and `release` jobs' `GITHUB_TOKEN` + (`packages: write`, actor `GITHUB_ACTOR`) can push to from `publish` + and `finalize`. - `CI_RUNNER_AMD64` / `CI_RUNNER_ARM64` (variables, optional) — runner labels for each arch leg, defaulting to `ubuntu-24.04` / `ubuntu-24.04-arm`. @@ -311,9 +320,13 @@ cleanly when the registry isn't configured. ## Do -- Give `release.yml` and `template-sync.yml` `contents: write` and - nothing broader (`template-sync.yml` also needs `pull-requests: - write`, for nothing else); leave `checks.yml` at `contents: read`. +- Give `release.yml` and `template-sync.yml` `contents: write`, with + exactly two additions and nothing else broader: `template-sync.yml` + also needs `pull-requests: write`, and `release.yml`'s `build` and + `release` jobs also have `packages: write`, so `make publish` and + `make finalize` can push to the repository's own GHCR packages with + `GITHUB_TOKEN` (no other job has it; the token is in `build`'s job + env, so every per-arch target can use it). Leave `checks.yml` at `contents: read`. - Keep `id-token: write` on `release.yml`'s `publish-packages` job only (PyPI, npm and crates.io trusted publishing via GitHub OIDC). GitHub permissions are per job, not per step, and that job's `if:` on the diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 001b7fd..c49e92c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,10 +80,15 @@ jobs: arch: ${{ fromJSON(needs.version.outputs.arches) }} # Deliberately no `id-token: write` and no package-registry secrets # here: this job runs for every instance, once per arch, and image - # publishing needs neither. Package publishing is the separate - # `publish-packages` job below. + # publishing needs neither. Package publishing (PyPI, npm, crates.io) + # is the separate `publish-packages` job below. `packages: write` lets + # `make publish` push to this repository's own GHCR packages + # (ghcr.io//...) with `GITHUB_TOKEN` (actor + # `GITHUB_ACTOR`) when the OCI_REGISTRY_* secrets are unset. The + # `release` job has it too, for `make finalize`'s manifest lists. permissions: contents: write + packages: write # renovate: datasource=github-runners depName=ubuntu runs-on: ${{ matrix.arch == 'arm64' && (vars.CI_RUNNER_ARM64 || 'ubuntu-24.04-arm') || (vars.CI_RUNNER_AMD64 || 'ubuntu-24.04') }} env: @@ -248,6 +253,12 @@ jobs: concurrency: group: release-${{ needs.version.outputs.tag }} cancel-in-progress: false + # `packages: write` lets `make finalize` combine per-arch tags into a + # multi-arch manifest list in this repository's own GHCR packages with + # `GITHUB_TOKEN`, as `build`'s `make publish` pushes them. + permissions: + contents: write + packages: write # renovate: datasource=github-runners depName=ubuntu runs-on: ${{ vars.CI_RUNNER || 'ubuntu-24.04' }} steps: diff --git a/docs/TEMPLATE.md b/docs/TEMPLATE.md index c7814ad..90d9c9b 100644 --- a/docs/TEMPLATE.md +++ b/docs/TEMPLATE.md @@ -135,9 +135,9 @@ unchanged across every instance), then drives these targets: | Target | Runs | Where | Env available | |---|---|---|---| | `release-arches` | once, first | host runner | none; must print a JSON array on stdout, e.g. `["amd64"]` | -| `build`, `sbom`, `release-assets`, `publish` | once **per arch**, in that order | host runner for that arch | `RELEASE_VERSION`, `RELEASE_TAG`, `RELEASE_ARCH`, `OCI_REGISTRY`, `OCI_IMAGE_NAME`, `OCI_REGISTRY_USERNAME`, `OCI_REGISTRY_PASSWORD`, all `GITHUB_*`; no package-registry variables or secrets, and no OIDC token | +| `build`, `sbom`, `release-assets`, `publish` | once **per arch**, in that order | host runner for that arch | `RELEASE_VERSION`, `RELEASE_TAG`, `RELEASE_ARCH`, `OCI_REGISTRY`, `OCI_IMAGE_NAME`, `OCI_REGISTRY_USERNAME`, `OCI_REGISTRY_PASSWORD`, all `GITHUB_*` (`GITHUB_TOKEN` has `packages: write`); no package-registry variables or secrets, and no OIDC token | | `release-check` | once, after all arches; gates the GitHub release and `publish-packages` (not the per-arch image `publish`, which already ran in `build`) | **inside the devcontainer** (`devcontainers/ci`), stack up | `RELEASE_VERSION`, `RELEASE_TAG`, `GITHUB_REPOSITORY`; **not** `RELEASE_ARCH` (it runs once, not per arch) and no `ARCHES`: to check each arch's files in `dist/`, iterate over `make -s release-arches` | -| `finalize` | once, after `release-check`; then the GitHub release is created | host runner | same as per-arch minus `RELEASE_ARCH` | +| `finalize` | once, after `release-check`; then the GitHub release is created | host runner | same as per-arch minus `RELEASE_ARCH` (`GITHUB_TOKEN` also has `packages: write`) | | `publish-packages` | once, after the GitHub release exists, **only if** `PYPI_PUBLISH`, `NPM_PUBLISH` or `CARGO_PUBLISH` is set | host runner (own job, with `id-token: write`) | `RELEASE_VERSION`, `RELEASE_TAG`, `PYPI_PUBLISH`, `NPM_PUBLISH`, `CARGO_PUBLISH`, all `GITHUB_*`, the OIDC token, `NPM_TOKEN` and `CARGO_REGISTRY_TOKEN` (bootstrap secrets; empty when unset, so treat empty as absent); `dist/` holds every arch leg's files | | `verify-published` | once, after `publish-packages` succeeded, **only if** a `*_PUBLISH` variable is set | host runner (own job, no secrets, no OIDC token) | `RELEASE_VERSION`, `RELEASE_TAG`, `PYPI_PUBLISH`, `NPM_PUBLISH`, `CARGO_PUBLISH`; `dist/` as for `publish-packages` | @@ -164,6 +164,23 @@ multi-arch manifest list. `RELEASE_VERSION` (e.g. `1.2.3` or embedding or tagging with the version. A single-arch no-op is the default: `release-arches` prints `["amd64"]`. +**Registry credentials for `publish` and `finalize`.** The per-arch job's +and the `release` job's `GITHUB_TOKEN` have `packages: write`, so an +instance's `publish` and `finalize` (e.g. a multi-arch manifest list) may +push to the repository's own GHCR packages (`ghcr.io//...`) without +`OCI_REGISTRY_USERNAME`/`OCI_REGISTRY_PASSWORD`: log in with username +`GITHUB_ACTOR` and password `GITHUB_TOKEN`. A package that the workflow +creates inherits the repository's permissions. A package that already +exists and isn't linked to the repository first needs the repository added +under the package's "Manage Actions access" settings, with write access. +The explicit secrets remain the way to reach any other registry, or a +package outside that repository's reach, and an instance that sets them +should prefer them. Base's own `publish` and `finalize` are no-ops and log +in nowhere, so each instance implements this fallback in its own +`Makefile`. Trade-off: the token is in the job env, so every per-arch +target (`build`, `sbom`, `release-assets`), not only `publish`, can write +those packages. + **Upgrading a package instance.** Before this contract, `publish` also uploaded packages, once per arch. Now `publish` is image-only and gets no `*_PUBLISH` variables, secrets or OIDC token. An instance that publishes diff --git a/scripts/develop.sh b/scripts/develop.sh index dad18db..5aeedb1 100644 --- a/scripts/develop.sh +++ b/scripts/develop.sh @@ -175,8 +175,11 @@ run_prek_installer() { env HOME=/home/vscode INSTALLER_NO_MODIFY_PATH=1 sh "$installer_script" } +# CLAUDE_CONFIG_DIR matches compose.yml's, so anything the installer writes +# lands where Claude Code will look for it at runtime. run_claude_installer() { - sudo -u vscode env HOME=/home/vscode bash "$installer_script" "$claude_code_version" + sudo -u vscode env HOME=/home/vscode CLAUDE_CONFIG_DIR=/home/vscode/.claude \ + bash "$installer_script" "$claude_code_version" } # xz-utils: Node's own release tarballs are .tar.xz. @@ -240,6 +243,11 @@ download_installer prek "https://github.com/j178/prek/releases/download/v${prek_ retry run_prek_installer ln -s /home/vscode/.local/bin/prek /usr/local/bin/prek +# Created here, owned by vscode, because a fresh claude-config volume +# (compose.yml) copies this directory's ownership and mode on first mount; +# left to the installer it could be missing or root-owned. 0700: it holds +# Claude Code's credentials. +sudo -u vscode install -d -m 0700 /home/vscode/.claude download_installer claude https://claude.ai/install.sh retry run_claude_installer ln -s /home/vscode/.local/bin/claude /usr/local/bin/claude