From ad88ce1be8757f7b167e8c5eb36e16287e7f393d Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:54:43 -0700 Subject: [PATCH] refactor(release): publish self-host container image as loopover-selfhost with old-name alias Publishes ghcr.io/jsonbored/loopover-selfhost as the primary self-host image going forward, while the existing ghcr.io/jsonbored/gittensory-selfhost tag keeps resolving to the byte-identical build through the deprecation window (#4777 removes it later). Both names come from a SINGLE docker buildx build/push -- docker/metadata-action's images: input now lists both repository names, so the same multi-arch manifest gets pushed under both, with no risk of the two tags ever drifting. Updates the release notes' pull command, deploy-selfhost-image.sh's default, .env.example's GITTENSORY_IMAGE example, and 3 self-hosting docs pages to present the new name as canonical while noting the old alias still works. Deliberately unchanged: the OCI label block's gittensory-orb title (overlaps the separately-tracked GitHub App slug, #4772), Sentry/OTEL release-naming defaults (separate identifier namespace), docs.self-hosting-release- checklist.tsx (a historical record of a specific past release, same reasoning as never editing CHANGELOG.md), and docker-compose.yml (its service builds locally, never pulls the ghcr path). Note for whoever runs the first release after this merges: GHCR defaults a brand-new package name to private on first push via the automatic GITHUB_TOKEN -- the new loopover-selfhost package will likely need its visibility manually set to Public in the org's Packages settings, or self-hosters following the new pull command will hit an auth error. Closes #4770. --- .env.example | 7 ++++-- .github/workflows/orb-beta-release.yml | 4 ++- .github/workflows/orb-stable-release-pr.yml | 4 ++- .github/workflows/release-selfhost.yml | 25 ++++++++++++++++--- .../routes/docs.self-hosting-operations.tsx | 9 +++++-- .../routes/docs.self-hosting-quickstart.tsx | 9 +++++-- .../src/routes/docs.self-hosting-releases.tsx | 14 ++++++++--- orb-manifest.json | 2 +- scripts/deploy-selfhost-image.sh | 11 +++++--- scripts/orb-release-core.mjs | 4 ++- test/unit/release-selfhost-notes.test.ts | 12 ++++++--- test/unit/selfhost-image-deploy.test.ts | 21 +++++++++++++++- test/unit/selfhost-sentry-release.test.ts | 16 +++++++++++- 13 files changed, 112 insertions(+), 26 deletions(-) diff --git a/.env.example b/.env.example index 9acbfb6e33..f5097a7e2c 100644 --- a/.env.example +++ b/.env.example @@ -244,9 +244,12 @@ GITTENSORY_REVIEW_DRAFT=false # # request-latency metrics to tell those two cases apart). REDIS_URL=redis://redis:6379 # REQUIRED for the self-host review runtime. The default compose stack # # starts Redis automatically; override for an external Redis. -# GITTENSORY_IMAGE=ghcr.io/jsonbored/gittensory-selfhost:latest # image used by scripts/deploy-selfhost-image.sh; +# GITTENSORY_IMAGE=ghcr.io/jsonbored/loopover-selfhost:latest # image used by scripts/deploy-selfhost-image.sh; # # pin production rollouts to a release tag such as :orb-v0.1.0 -# # or to an immutable @sha256 digest. +# # or to an immutable @sha256 digest. #4770: the pre-rename +# # ghcr.io/jsonbored/gittensory-selfhost name is a deprecated +# # alias of the same image during the deprecation window (#4777) +# # -- an existing pin to it keeps working unmodified. # GITHUB_CACHE_TTL_SECONDS=20 # Enables the Redis-backed GitHub GET-response cache when >0; set 0 # # to disable. NOT a per-entry TTL duration — each cached class # # resolves its own TTL below (GITHUB_BRANCH_PROTECTION_CACHE_TTL_SECONDS diff --git a/.github/workflows/orb-beta-release.yml b/.github/workflows/orb-beta-release.yml index 53ece0f8b6..977fc28457 100644 --- a/.github/workflows/orb-beta-release.yml +++ b/.github/workflows/orb-beta-release.yml @@ -1,4 +1,6 @@ -# Automated ORB (self-host container image, ghcr.io/jsonbored/gittensory-selfhost) beta channel. +# Automated ORB (self-host container image, ghcr.io/jsonbored/loopover-selfhost -- #4770: the pre-rename +# ghcr.io/jsonbored/gittensory-selfhost name remains a deprecated alias of the same build during the +# deprecation window tracked by #4777) beta channel. # Daily (or on demand via workflow_dispatch), checks whether any image-relevant commit has landed # since the last orb-v tag (scripts/check-orb-release-due.mjs / scripts/orb-release-core.mjs) and, # if so, cuts the next `orb-vX.Y.Z-beta.N` tag and dispatches release-selfhost.yml to build + publish diff --git a/.github/workflows/orb-stable-release-pr.yml b/.github/workflows/orb-stable-release-pr.yml index 293b6fb40a..99a948d0f8 100644 --- a/.github/workflows/orb-stable-release-pr.yml +++ b/.github/workflows/orb-stable-release-pr.yml @@ -1,4 +1,6 @@ -# Automated ORB (self-host container image, ghcr.io/jsonbored/gittensory-selfhost) STABLE-channel Release PR -- +# Automated ORB (self-host container image, ghcr.io/jsonbored/loopover-selfhost -- #4770: the pre-rename +# ghcr.io/jsonbored/gittensory-selfhost name remains a deprecated alias of the same build during the +# deprecation window tracked by #4777) STABLE-channel Release PR -- # the release-please-equivalent for ORB, whose cross-cutting image-relevant scoping (src/** shared with # UI/MCP-only subtrees it must exclude -- see orb-release-core.mjs's IMAGE_RELEVANT_PREFIXES/EXCLUDED_PREFIXES) # doesn't fit release-please's directory-component model the way packages/gittensory-mcp and diff --git a/.github/workflows/release-selfhost.yml b/.github/workflows/release-selfhost.yml index e62f7b8a83..05e5988de7 100644 --- a/.github/workflows/release-selfhost.yml +++ b/.github/workflows/release-selfhost.yml @@ -3,7 +3,12 @@ # # git tag orb-v0.1.0 && git push origin orb-v0.1.0 # -# Pull: docker pull ghcr.io//gittensory-selfhost:orb-v0.1.0 +# Pull: docker pull ghcr.io//loopover-selfhost:orb-v0.1.0 +# +# #4770: `ghcr.io//gittensory-selfhost` is a deprecated alias of the SAME image, published from this +# same build/push so both names resolve to the byte-identical digest -- see the "Image metadata" step below. +# Self-hosters pinned to the old name keep working unmodified during the deprecation window; do not remove +# that alias without the sign-off tracked by #4777. # # Prerelease tags (#1937): orb-v0.1.0-rc.1 / orb-v0.1.0-beta.1 run the identical pipeline but never move # `latest` and are marked prerelease on the GitHub Release -- for beta-testing an image before it becomes @@ -217,11 +222,21 @@ jobs: echo "GTORBTAGS" } >> "$GITHUB_OUTPUT" + # #4770: publish under BOTH the new "loopover-selfhost" name (primary going forward, listed first) and + # the legacy "gittensory-selfhost" name (deprecated alias) from this SAME buildx build/push below -- + # docker/metadata-action applies the identical `tags:` set to every entry in a multi-line `images:` + # list, so both names resolve to the byte-identical multi-arch manifest with no second build and no + # extra registry round-trip. Self-hosters who pinned the old image reference in their own deployment + # config must keep working unmodified until the deprecation window closes. Removal of the alias is + # tracked by #4777 ("retire every deprecation shim") and requires an explicit, separately-announced + # window before it's dropped -- do not remove the second `images:` line without that sign-off. - name: Image metadata id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 with: - images: ghcr.io/${{ github.repository_owner }}/gittensory-selfhost + images: | + ghcr.io/${{ github.repository_owner }}/loopover-selfhost + ghcr.io/${{ github.repository_owner }}/gittensory-selfhost tags: ${{ steps.tags.outputs.list }} labels: | org.opencontainers.image.title=gittensory-orb @@ -333,12 +348,16 @@ jobs: Gittensory Orb container image: \`\`\`bash - docker pull ghcr.io/${REPOSITORY_OWNER_LOWER}/gittensory-selfhost:${RELEASE_TAG} + docker pull ghcr.io/${REPOSITORY_OWNER_LOWER}/loopover-selfhost:${RELEASE_TAG} \`\`\` Multi-arch (linux/amd64 + linux/arm64). See https://gittensory.aethereal.dev/docs/maintainer-self-hosting for setup. Includes the Claude Code / Codex subscription CLIs by default; credentials stay runtime-only. Sentry release id baked into the image: \`${RELEASE_ID}\`. + + #4770: \`ghcr.io/${REPOSITORY_OWNER_LOWER}/gittensory-selfhost:${RELEASE_TAG}\` (the pre-rename name) + still resolves to this identical build/digest during the deprecation window -- no action needed if + you're already pinned to it. Track eventual removal via #4777. EOF )" # #1937: a prerelease tag never becomes the repo's "Latest release" and is visibly marked as such diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx index 02b12dae85..ec383dddcb 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx @@ -976,9 +976,14 @@ SENTRY_ORG_SLUG= ./scripts/deploy-selfhost-image.sh # Pin an exact release tag or content digest -./scripts/deploy-selfhost-image.sh ghcr.io/jsonbored/gittensory-selfhost:orb-v0.1.0 -GITTENSORY_IMAGE=ghcr.io/jsonbored/gittensory-selfhost@sha256:... ./scripts/deploy-selfhost-image.sh`} +./scripts/deploy-selfhost-image.sh ghcr.io/jsonbored/loopover-selfhost:orb-v0.1.0 +GITTENSORY_IMAGE=ghcr.io/jsonbored/loopover-selfhost@sha256:... ./scripts/deploy-selfhost-image.sh`} /> +

+ ghcr.io/jsonbored/gittensory-selfhost (the pre-rename name) remains a + deprecated alias of the identical image during the deprecation window — an existing pin to + it keeps resolving unmodified. +

The pull always runs with --policy always, so re-running the script against an unchanged tag is safe: if the registry has nothing new, it just restarts the same image and diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-quickstart.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-quickstart.tsx index ab8204e5cb..4f17aa56b8 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-quickstart.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-quickstart.tsx @@ -116,9 +116,14 @@ curl http://localhost:8787/ready`}

+

+ ghcr.io/jsonbored/gittensory-selfhost (the pre-rename name) remains available + as a deprecated alias of the identical image during the deprecation window — existing pins + keep working unmodified. +

Contributors and anyone customizing the Dockerfile can still build locally —{" "} docker compose up -d --build builds the gittensory service from diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-releases.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-releases.tsx index a9314e83a0..3fd61a3ca3 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-releases.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-releases.tsx @@ -52,9 +52,15 @@ function SelfHostingReleases() { /> + + ghcr.io/jsonbored/loopover-selfhost is the current name. The pre-rename{" "} + ghcr.io/jsonbored/gittensory-selfhost remains available as a deprecated alias + of the byte-identical image during the deprecation window — existing pins keep working + unmodified. +

Prerelease (beta/rc) images

@@ -65,7 +71,7 @@ docker pull ghcr.io/jsonbored/gittensory-selfhost:latest`}

Stable release behavior is unchanged: a plain X.Y.Z tag still moves{" "} @@ -96,7 +102,7 @@ docker pull ghcr.io/jsonbored/gittensory-selfhost:latest`} { @@ -192,7 +196,7 @@ describe('release-selfhost.yml "GitHub Release" step changelog generation', () = const harness = createHarness({ tagList: ["orb-v0.2.0", "orb-v0.1.0"] }); // changelogBody omitted -> API call exits 1 const r = harness.run(); expect(r.status).toBe(0); - expect(r.notesPassed).toContain("docker pull ghcr.io/jsonbored/gittensory-selfhost:orb-v0.2.0"); + expect(r.notesPassed).toContain("docker pull ghcr.io/jsonbored/loopover-selfhost:orb-v0.2.0"); // A silent empty changelog would be indistinguishable from a genuinely empty PR range -- the run // log must say the API call itself failed. expect(r.stdout).toContain("::warning::Fetching the release changelog"); diff --git a/test/unit/selfhost-image-deploy.test.ts b/test/unit/selfhost-image-deploy.test.ts index c325c33d2c..15061b797e 100644 --- a/test/unit/selfhost-image-deploy.test.ts +++ b/test/unit/selfhost-image-deploy.test.ts @@ -12,7 +12,11 @@ import { spawnSync } from "node:child_process"; import { describe, expect, it } from "vitest"; const scriptPath = resolve("scripts/deploy-selfhost-image.sh"); -const defaultImage = "ghcr.io/jsonbored/gittensory-selfhost:latest"; +const defaultImage = "ghcr.io/jsonbored/loopover-selfhost:latest"; +// #4770: the pre-rename name is a deprecated alias of the identical image during the deprecation +// window -- an operator who pinned it explicitly (CLI arg, env var, or .env value) must keep +// resolving to that exact string unmodified, even though DEFAULT_IMAGE above now points at the new name. +const legacyPinnedImage = "ghcr.io/jsonbored/gittensory-selfhost:orb-v0.1.0"; interface RunOptions { args?: string[]; @@ -163,6 +167,21 @@ describe("self-host image deploy script", () => { } }); + // REGRESSION (#4770): DEFAULT_IMAGE moved to the new "loopover-selfhost" name, but a self-hoster who + // explicitly pinned the pre-rename "gittensory-selfhost" image (as a CLI argument, GITTENSORY_IMAGE env + // var, or .env value) must keep resolving to that exact string, unmodified -- the deprecation alias is + // guaranteed by the release workflow's dual-tag push, not by rewriting an operator's existing pin here. + it("passes an explicit pre-rename image reference through unchanged despite the new default", () => { + const { harness, result } = runHarness({ args: [legacyPinnedImage] }); + try { + expect(result.status, result.stderr).toBe(0); + expect(readFileSync(harness.envPath, "utf8")).toContain(`GITTENSORY_IMAGE=${legacyPinnedImage}`); + expect(harness.readImages()).toContain(`image: "${legacyPinnedImage}"`); + } finally { + harness.cleanup(); + } + }); + it("persists the image only after the service reports healthy", () => { const image = "ghcr.io/jsonbored/gittensory-selfhost:ordered"; const { harness, result } = runHarness({ args: [image], envFile: "EXISTING=1\n" }); diff --git a/test/unit/selfhost-sentry-release.test.ts b/test/unit/selfhost-sentry-release.test.ts index 238323921e..b295bf1ae0 100644 --- a/test/unit/selfhost-sentry-release.test.ts +++ b/test/unit/selfhost-sentry-release.test.ts @@ -40,7 +40,21 @@ describe("self-host Sentry release wiring", () => { // notes block doesn't go through it). expect(releaseWorkflow).toContain('REPOSITORY_OWNER_LOWER="${REPOSITORY_OWNER,,}"'); expect(releaseWorkflow).toContain( - "docker pull ghcr.io/${REPOSITORY_OWNER_LOWER}/gittensory-selfhost:${RELEASE_TAG}", + "docker pull ghcr.io/${REPOSITORY_OWNER_LOWER}/loopover-selfhost:${RELEASE_TAG}", + ); + // #4770: the release notes must also point out that the pre-rename image name still resolves to + // the identical build during the deprecation window (tracked for eventual removal by #4777). + expect(releaseWorkflow).toContain( + "ghcr.io/${REPOSITORY_OWNER_LOWER}/gittensory-selfhost:${RELEASE_TAG}", + ); + expect(releaseWorkflow).toContain("#4777"); + // The "Image metadata" step must push BOTH names from the same buildx build so the two tags share + // a byte-identical digest -- no second build, no drift between them. + expect(releaseWorkflow).toContain( + "ghcr.io/${{ github.repository_owner }}/loopover-selfhost", + ); + expect(releaseWorkflow).toContain( + "ghcr.io/${{ github.repository_owner }}/gittensory-selfhost", ); expect(releaseWorkflow).not.toContain('"selfhost-v*"'); expect(releaseWorkflow).not.toContain('VERSION="${REF_NAME#selfhost-v}"');