Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc53877328 | |||
| c42b237d30 | |||
| b7197e88b0 |
@@ -565,16 +565,19 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- base-decide
|
- base-decide
|
||||||
- build-variant
|
- build-variant
|
||||||
# Skip on cache-hit base builds: when need_build=false, base-latest
|
# Run on every tag release (and on promote_latest=true dispatches).
|
||||||
# already points at the same digest as base-<hash>, so the retag is
|
# The job-level gate deliberately does NOT key off need_build anymore:
|
||||||
# a tautology and any transient failure of it is purely cosmetic.
|
# the actual no-op optimization moved INTO the step as a digest compare
|
||||||
# Manual workflow_dispatch with promote_latest=true overrides this
|
# (see below). Keying the gate on need_build was wrong because a prior
|
||||||
# gate as an escape hatch (e.g., if base-latest got hand-deleted).
|
# dry-run dispatch (promote_latest=false) can pre-build+push base-<hash>,
|
||||||
|
# making need_build=false on the subsequent tag run even though
|
||||||
|
# base-latest is still stale — the old gate then skipped promotion and
|
||||||
|
# left base-latest pointing at the PREVIOUS base. (Observed 2026-06-27,
|
||||||
|
# v1.2.3: dry-run-first release left base-latest one base behind.)
|
||||||
if: |
|
if: |
|
||||||
always() &&
|
always() &&
|
||||||
needs.build-variant.result == 'success' &&
|
needs.build-variant.result == 'success' &&
|
||||||
(inputs.promote_latest == 'true' ||
|
(inputs.promote_latest == 'true' || github.ref_type == 'tag')
|
||||||
(github.ref_type == 'tag' && needs.base-decide.outputs.need_build == 'true'))
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
@@ -596,11 +599,31 @@ jobs:
|
|||||||
crane auth login docker.io \
|
crane auth login docker.io \
|
||||||
-u ${{ vars.DOCKERHUB_USERNAME }} \
|
-u ${{ vars.DOCKERHUB_USERNAME }} \
|
||||||
-p "${{ secrets.DOCKERHUB_TOKEN }}"
|
-p "${{ secrets.DOCKERHUB_TOKEN }}"
|
||||||
- name: Re-tag base-<hash> as base-latest
|
- name: Re-tag base-<hash> as base-latest (only if stale)
|
||||||
|
env:
|
||||||
|
BASE_HASH_REF: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
||||||
|
BASE_LATEST_REF: ${{ env.IMAGE }}:base-latest
|
||||||
run: |
|
run: |
|
||||||
crane copy \
|
set -euo pipefail
|
||||||
${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }} \
|
# Correctness invariant: after a release, base-latest must resolve to
|
||||||
${{ env.IMAGE }}:base-latest
|
# the SAME digest as the base-<hash> the just-built variants were
|
||||||
|
# FROM. Compare digests rather than trusting need_build — a prior
|
||||||
|
# dry-run dispatch can pre-build base-<hash>, so need_build=false on
|
||||||
|
# the tag run does NOT imply base-latest is already current. When the
|
||||||
|
# digests already match (genuine cache-hit release) this is a no-op,
|
||||||
|
# so we skip the crane copy entirely — preserving the original
|
||||||
|
# "don't do a tautological retag" intent and avoiding any cosmetic
|
||||||
|
# transient-failure exposure on releases that change nothing.
|
||||||
|
want=$(crane digest "${BASE_HASH_REF}")
|
||||||
|
have=$(crane digest "${BASE_LATEST_REF}" 2>/dev/null || echo "")
|
||||||
|
echo "base-<hash> digest: ${want}"
|
||||||
|
echo "base-latest digest: ${have:-<absent>}"
|
||||||
|
if [ "${want}" = "${have}" ]; then
|
||||||
|
echo "base-latest already current; nothing to promote."
|
||||||
|
else
|
||||||
|
echo "Promoting base-latest -> ${BASE_HASH_REF}"
|
||||||
|
crane copy "${BASE_HASH_REF}" "${BASE_LATEST_REF}"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Phase 6: update Hub description (only on real release runs) ────
|
# ── Phase 6: update Hub description (only on real release runs) ────
|
||||||
update-description:
|
update-description:
|
||||||
|
|||||||
@@ -13,6 +13,46 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Global gitignore baked into the image.** A `~/.gitignore_global`
|
||||||
|
(`*.bak`, `*.bak.*`, `*~`, `*.orig`, `*.swp`, `*.tmp`) is seeded into the home
|
||||||
|
dir from `/etc/skel-devbox/` on first boot (seed-if-absent, like
|
||||||
|
`.bash_aliases`/`.inputrc`, so user edits survive recreate) and wired via
|
||||||
|
`git config --global core.excludesFile`. Personal/tooling backup artifacts are
|
||||||
|
now ignored across all repos in the container without per-repo `.gitignore`
|
||||||
|
entries. The `core.excludesFile` wiring is skipped if the user already set one.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Secrets are now delivered to the container via `env_file: .env` only; the
|
||||||
|
`environment:` block no longer re-declares `GITEA_ACCESS_TOKEN`,
|
||||||
|
`GITEA_HOST`, or `GITHUB_PERSONAL_ACCESS_TOKEN`.** An `environment:` entry
|
||||||
|
both overrides `env_file:` and is interpolated from the host shell, so a
|
||||||
|
stale shell export (e.g. one auto-loaded by an opencode/dotenv hook) would
|
||||||
|
silently shadow the value in your `.env` — an updated token in `.env` never
|
||||||
|
reached the container. Delivering secrets via `env_file` only decouples the
|
||||||
|
container from whatever the host shell happens to export. No action needed:
|
||||||
|
`.env.example` already documents every supported variable. Affects
|
||||||
|
`docker-compose.yml` and the README “basic shape” snippet.
|
||||||
|
|
||||||
|
### Fixed (CI)
|
||||||
|
|
||||||
|
- **`promote-base-latest` now re-points `base-latest` reliably after a
|
||||||
|
dry-run-first release.** The job's gate previously required
|
||||||
|
`need_build == 'true'`, on the assumption that `need_build == false`
|
||||||
|
implied `base-latest` was already current. That assumption breaks when a
|
||||||
|
`workflow_dispatch` dry-run (`promote_latest=false`) pre-builds and pushes
|
||||||
|
`base-<hash>` first: the subsequent tag run then sees `need_build == false`
|
||||||
|
(probe hit) and **skipped** promotion, leaving `base-latest` pointing at the
|
||||||
|
*previous* base. (Observed 2026-06-27 releasing v1.2.3 via dry-run-then-tag
|
||||||
|
— `base-latest` ended up one base behind, lacking the mempalace self-heal.)
|
||||||
|
Now the gate runs on every tag release (or `promote_latest=true` dispatch),
|
||||||
|
and the no-op optimization moved **into** the step as a `crane digest`
|
||||||
|
compare: it re-tags only when `base-latest` actually differs from the
|
||||||
|
released `base-<hash>`, so genuine cache-hit releases stay a no-op while
|
||||||
|
stale aliases get corrected. No image-content change; base hash unaffected.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v1.2.3 — 2026-06-27
|
## v1.2.3 — 2026-06-27
|
||||||
|
|||||||
@@ -479,6 +479,7 @@ ENV PATH="/home/${USER_NAME}/.pi/npm-global/bin:${PATH}"
|
|||||||
RUN mkdir -p /etc/skel-devbox
|
RUN mkdir -p /etc/skel-devbox
|
||||||
COPY rootfs/home/developer/.bash_aliases /etc/skel-devbox/.bash_aliases
|
COPY rootfs/home/developer/.bash_aliases /etc/skel-devbox/.bash_aliases
|
||||||
COPY rootfs/home/developer/.inputrc /etc/skel-devbox/.inputrc
|
COPY rootfs/home/developer/.inputrc /etc/skel-devbox/.inputrc
|
||||||
|
COPY rootfs/home/developer/.gitignore_global /etc/skel-devbox/.gitignore_global
|
||||||
|
|
||||||
# ── Entrypoint ────────────────────────────────────────────────────────
|
# ── Entrypoint ────────────────────────────────────────────────────────
|
||||||
COPY rootfs/usr/local/lib/pi-devbox/ /usr/local/lib/pi-devbox/
|
COPY rootfs/usr/local/lib/pi-devbox/ /usr/local/lib/pi-devbox/
|
||||||
|
|||||||
@@ -319,9 +319,10 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- TERM=xterm-256color
|
- TERM=xterm-256color
|
||||||
# - STUDIO_EXPOSE=1 # -studio only: auto-start the socat bridge on boot
|
# - STUDIO_EXPOSE=1 # -studio only: auto-start the socat bridge on boot
|
||||||
- GITEA_ACCESS_TOKEN=${GITEA_ACCESS_TOKEN:-}
|
# Secrets (GITEA_*, GITHUB_*, …) come from env_file: .env above — not
|
||||||
- GITEA_HOST=${GITEA_HOST:-}
|
# duplicated here. An environment: entry overrides env_file and is
|
||||||
- GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN:-}
|
# interpolated from the host shell, so a stale shell export would
|
||||||
|
# silently shadow your .env. See .env.example for the full list.
|
||||||
volumes:
|
volumes:
|
||||||
# Workspace: your host source tree
|
# Workspace: your host source tree
|
||||||
- ${WORKSPACE_PATH:-.}:/workspace
|
- ${WORKSPACE_PATH:-.}:/workspace
|
||||||
|
|||||||
+7
-3
@@ -31,9 +31,13 @@ services:
|
|||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- TERM=xterm-256color
|
- TERM=xterm-256color
|
||||||
- GITEA_ACCESS_TOKEN=${GITEA_ACCESS_TOKEN:-}
|
# Secrets (GITEA_*, GITHUB_*, and any others) are delivered to the
|
||||||
- GITEA_HOST=${GITEA_HOST:-}
|
# container via `env_file: .env` above — do NOT duplicate them here.
|
||||||
- GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN:-}
|
# An `environment:` entry overrides env_file AND is interpolated from
|
||||||
|
# the host shell, so a stale shell export (e.g. one auto-loaded by a
|
||||||
|
# dotenv hook) would silently shadow the value in your .env. Keeping
|
||||||
|
# secrets env_file-only decouples the container from the host shell.
|
||||||
|
# See .env.example for the full list of supported variables.
|
||||||
volumes:
|
volumes:
|
||||||
# Host workspace — mount your project here
|
# Host workspace — mount your project here
|
||||||
- ${WORKSPACE_PATH:-.}:/workspace
|
- ${WORKSPACE_PATH:-.}:/workspace
|
||||||
|
|||||||
+7
-1
@@ -33,7 +33,7 @@ fi
|
|||||||
# directly.
|
# directly.
|
||||||
SKEL_DIR="/etc/skel-devbox"
|
SKEL_DIR="/etc/skel-devbox"
|
||||||
if [ -d "$SKEL_DIR" ]; then
|
if [ -d "$SKEL_DIR" ]; then
|
||||||
for f in .bash_aliases .inputrc; do
|
for f in .bash_aliases .inputrc .gitignore_global; do
|
||||||
if [ -f "$SKEL_DIR/$f" ] && [ ! -e "$HOME/$f" ]; then
|
if [ -f "$SKEL_DIR/$f" ] && [ ! -e "$HOME/$f" ]; then
|
||||||
cp "$SKEL_DIR/$f" "$HOME/$f"
|
cp "$SKEL_DIR/$f" "$HOME/$f"
|
||||||
fi
|
fi
|
||||||
@@ -91,6 +91,12 @@ fi
|
|||||||
if [ -n "${GIT_USER_EMAIL:-}" ] && ! git config --global user.email &>/dev/null; then
|
if [ -n "${GIT_USER_EMAIL:-}" ] && ! git config --global user.email &>/dev/null; then
|
||||||
git config --global user.email "$GIT_USER_EMAIL"
|
git config --global user.email "$GIT_USER_EMAIL"
|
||||||
fi
|
fi
|
||||||
|
# Global gitignore for personal/tooling artifacts (*.bak, *~, *.orig, ...).
|
||||||
|
# Seeded above into $HOME/.gitignore_global from /etc/skel-devbox. Point git at
|
||||||
|
# it only if the user has not already set their own core.excludesFile.
|
||||||
|
if [ -f "$HOME/.gitignore_global" ] && ! git config --global core.excludesFile &>/dev/null; then
|
||||||
|
git config --global core.excludesFile "$HOME/.gitignore_global"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── pi: deploy toolkit + extensions + mempalace bridge ─────────────
|
# ── pi: deploy toolkit + extensions + mempalace bridge ─────────────
|
||||||
# pi is always installed in pi-devbox; no INSTALL_PI guard needed.
|
# pi is always installed in pi-devbox; no INSTALL_PI guard needed.
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Global gitignore — personal/tooling artifacts (applies to all repos in the container)
|
||||||
|
# Seeded into $HOME/.gitignore_global by entrypoint-user.sh and wired via
|
||||||
|
# `git config --global core.excludesFile`. Edit freely; it is yours after first boot.
|
||||||
|
|
||||||
|
# backup / editor / merge artifacts
|
||||||
|
*.bak
|
||||||
|
*.bak.*
|
||||||
|
*~
|
||||||
|
*.orig
|
||||||
|
*.swp
|
||||||
|
*.tmp
|
||||||
Reference in New Issue
Block a user