A branch is a moving name

R7K1 stopped treating a Git branch as a preview identity and separated source revision, declared inputs, artifact, environment, and route.

R7K1's first preview URL was derived from a Git branch:

feature/new-search -> new-search.preview.example.invalid

The mapping felt natural. A branch represented proposed work, and the URL gave that work a place to be reviewed.

Then the branch moved.

One reviewer still saw a container built from commit A. Another triggered a build from commit B. The route changed to B before its database fixture was ready, failed, and fell back to the older container. Both people said they were reviewing feature/new-search.

The branch name had become a label for several different things and an identity for none of them.

A branch points; it does not preserve

In Git, a branch is a movable reference to a commit. Pushing another commit advances the reference. Force-updating can move it elsewhere. Deleting the branch removes the convenient name while commits may remain reachable through other references.

That behavior is useful for development and unsuitable as the only key for a built artifact. An artifact needs to say which immutable source revision and other inputs produced its contents.

R7K1 separated:

branch: reviewer-friendly moving proposal name
commit: immutable source-tree reference
build inputs: source plus lockfile, recipe, base image, configuration
artifact: immutable output of those inputs
environment: running instance of an artifact with runtime state
route: current reviewer entry point

The branch could keep moving. Every other layer received its own identity and relationship.

The preview URL remained friendly. It stopped pretending to tell the whole story.

The first artifact tag repeated the mistake

The container image was tagged with a sanitized branch name. Rebuilding replaced the tag's target. A deployment log saying new-search:latest could not identify which bytes had run.

R7K1 tagged images with build identity tied to commit and declared build inputs. The branch label remained additional metadata and a convenient alias.

artifact: harbor/app:build-9f41c2
source commit: 3a9e...
proposal branch at request time: feature/new-search

An alias such as branch-new-search could move after a successful build, but routes and environments recorded the immutable artifact they actually used. Rollback selected a prior artifact ID, not the previous meaning of the branch tag.

The registry's content identity or digest provided stronger evidence when available. Human tags supported navigation; immutable identifiers supported trust.

I adopted a rule: mutable names may discover artifacts, but they may not be the only evidence inside a receipt.

A commit was still not a complete build identity

Building the same commit on two days could produce different output if dependencies floated, the base image changed, or build-time configuration differed.

The first manifest said only commit: 3a9e. That made source traceable and environment drift invisible.

R7K1 recorded declared inputs:

source commit
dependency lockfile digest
build recipe digest
base image identity
compiler and build-tool version
public build-time configuration
fixture schema version

Secrets were not copied into the manifest. Their identities or policy versions could be recorded where they affected behavior without exposing values.

The artifact ID initially used a generated build number with the input manifest attached. Later, content-addressed identity made reuse safer. The important first step was acknowledging that commit and artifact were different nouns.

Git preserved source. It did not freeze every external input a build consumed.

The environment had identity beyond its artifact

Two preview environments could run the same application image with different fixtures, runtime configuration, database migrations, or creation times.

R7K1 assigned an environment ID and stored a receipt:

interface PreviewEnvironment {
  environmentId: string;
  artifactId: string;
  fixtureId: string;
  configurationProfile: string;
  route: string;
  createdAt: string;
  expiresAt: string;
  lifecycleState: string;
}

The environment ID named one provisioned instance. Replacing it under the branch route created another environment or a new recorded generation, even when the friendly URL stayed the same.

This distinction made bug reports reproducible. “Broken on new-search” became “environment env-271 running artifact build-9f41c2 with fixture catalog-v4.”

The route could answer with those identifiers in a protected diagnostics panel. Reviewers did not have to infer them from a branch badge.

Runtime state deserved identity because the artifact was only one of its inputs.

The route was a mutable pointer

R7K1 treated the branch preview URL like M31V's active-release pointer. It mapped a friendly proposal name to one verified environment.

Changing the mapping was an activation step:

provision environment
wait for application readiness
run route smoke checks
atomically update branch route
retain previous mapping for bounded rollback

An environment could build and provision without becoming visible at the friendly route. A failed candidate left reviewers on the previous verified environment with a notice that a newer attempt had failed.

The route record included generation and expected predecessor. Two concurrent builds could not activate in completion order accidentally. A newer source request invalidated authority of an older late build unless a person explicitly selected it.

The URL remained stable while its target changed deliberately. That mutability became safe once it was modeled instead of hidden.

Build request captured the branch moment

When a webhook said a branch had updated, R7K1 resolved the branch to a commit immediately and stored both in a build request.

request ID: req-781
repository: harbor-demo
branch: feature/new-search
resolved commit: 3a9e...
requested at: 2016-01-19T14:20:00Z

Every later step used the stored commit, not “whatever the branch points to now.” Retrying the request rebuilt the same declared source. A new push created a new request.

This avoided a race where a queued build began after another push and silently built different code under the old request's logs.

If the commit could no longer be fetched because retention or repository policy changed, the request failed with that evidence. It did not fall back to current branch head.

Capturing the moving reference at the boundary turned an event into a reproducible intent.

A force push could replace the visible branch history. Existing artifacts and environments remained tied to commits no longer reachable from the current branch head.

R7K1 did not delete them immediately or relabel them as current. The branch timeline showed that an earlier generation came from a superseded source line. Review routes moved only after a successful build of the new head.

Cleanup policy considered artifact references, active environments, review pins, and expiry—not branch reachability alone. An environment used in a bug report should not vanish because the branch was rewritten.

The UI avoided implying that every prior environment was an ancestor of the current branch. It showed commit IDs and request chronology separately.

This preserved evidence without turning R7K1 into a permanent Git archive. Artifacts expired according to lifecycle once no active review or rollback needed them.

Moving source history and running environment history had related but different retention rules.

Pull request numbers were also insufficient

A review item or pull request number was more stable than a branch name and still contained many commits over time. Reopening it could introduce another sequence. One number identified a conversation, not one candidate.

R7K1 used review identity as the parent and build requests as generations:

review 42
  generation 7 -> commit A -> artifact X -> environment M
  generation 8 -> commit B -> artifact Y -> environment N

The friendly URL could represent the active generation. A reviewer could open an older generation explicitly for comparison.

Comments or test evidence linked to generation and environment, preventing a passing result on A from appearing as proof for B.

This pattern later generalized beyond code review. A task, proposal, or workflow can be stable while its candidate contents change. Evidence belongs to the candidate, not only to the conversation.

Reuse needed input equality, not name equality

R7K1 wanted to avoid rebuilding identical work. The first cache looked for an existing image with the branch tag. That reused stale output after the branch moved and missed reuse when two branches pointed to the same commit and inputs.

The reuse key came from declared build inputs. If source, lockfile, recipe, base image, and build configuration matched, an existing verified artifact could satisfy the request regardless of branch name.

Environment reuse required more: artifact, fixture, configuration, isolation policy, and lifecycle state needed to match. A running environment near expiry might be cheaper to replace than extend under unclear ownership.

Reuse returned a receipt naming the prior artifact and its verification. It did not silently skip work because a tag existed.

Separating names improved both safety and efficiency. Mutable labels are poor cache keys because they conflate human continuity with content equality.

Status pages showed the chain

The preview header initially displayed only branch name. R7K1 expanded it into a concise chain:

feature/new-search
commit 3a9e · build 9f41c2 · environment env-271
ready · expires in 18 hours

Details showed fixture, configuration profile, build request, activation time, previous route target, and verification links. Public preview data avoided secrets and sensitive repository information according to access policy.

The chain let a reviewer answer whether the page matched the commit under discussion. A mismatch became visible before feedback attached to the wrong candidate.

Headers and logs used the same identifiers. UI labels did not invent a parallel naming system.

Observability followed the modeled entities, making identity useful outside the database.

Build logs belonged to a build request and artifact attempt. Provisioning logs belonged to an environment. Route changes belonged to activation records. Application logs belonged to one environment generation.

The branch view aggregated them without merging them into one stream called new-search.

This prevented a late build failure from appearing beside application errors from the currently active environment with no distinction. It also made retention specific: old build logs could expire after their evidence window while active environment logs followed runtime policy.

Every record included parent relationships. Starting from a route incident, R7K1 could walk to environment, artifact, build request, commit, and branch event.

Names became navigation across evidence rather than buckets that erased provenance.

Tests attacked identity races

The simulator produced sequences that branch-name designs mishandled:

commit A request starts slowly
commit B request starts and finishes
B environment becomes ready and route activates
A build finishes later

Expected result: A artifact remains inspectable, but its request lacks authority to replace B at the branch route.

Other cases force-pushed the branch, retried an old request after the branch moved, reused an artifact across two branches, and provisioned two environments from one artifact with different fixtures.

Assertions checked entity relationships and route generation, not only final URL response. A route that happened to point at B after an unsafe race could still conceal incorrect activation history.

The tests made identity a protocol invariant rather than a naming convention.

After finding the branch problem, it would have been easy to replace every label with hashes. Reviewers needed memorable navigation.

R7K1 kept sanitized branch slugs, review titles, and readable environment labels. It treated collisions explicitly and appended a short stable suffix where necessary. The full canonical identity remained in receipts.

Deleting a branch could retire its friendly route after a grace period while pinned generation URLs remained available according to policy. Renaming a branch created a new label relationship without rewriting artifact history.

Human and machine identity served different tasks. A branch name was excellent for “show me the current proposal from this line of work.” It was poor for “prove which bytes and state produced this response.”

The design needed both, connected rather than confused.

Branch names sometimes contained issue descriptions, customer-like test labels, or other context that did not belong in a broadly visible hostname. Simply lowercasing and replacing slashes with hyphens solved DNS syntax and not disclosure.

R7K1 classified preview access before creating the route. Protected reviews could use a readable sanitized label inside an authenticated index while the external hostname used an opaque environment suffix. Public demonstration branches required names safe for public logs, certificates, and DNS history.

Sanitization also handled collisions. feature/a_b and feature/a-b could collapse to the same slug. The route allocator appended a short review identity and stored the original branch only in protected metadata.

The system never treated a route slug as a reversible encoding of repository data. It was a display label with uniqueness and disclosure policy.

Certificate automation and DNS providers could retain requested names beyond environment deletion. That tail influenced what the project allowed into hostnames.

Friendly identity improved collaboration only when it did not publish more context than the preview itself was authorized to reveal.

Status stopped collapsing transition and outcome

The branch page originally showed deploying, deployed, or failed. Those words mixed build, provisioning, readiness, and route activation.

After separating entities, the status page could say:

build: verified
environment: provisioning
application: not ready
branch route: still serving generation 7
requested generation: 8

A failure named its boundary. A build failure left no environment. A provisioning failure left the prior route. A readiness failure created an inspectable but inactive environment. An activation failure required checking which generation the route actually selected.

This prevented one red badge from suggesting that the current preview was down when only the next candidate had failed. It also stopped a green deployed badge from claiming readiness based on container creation.

The branch remained the page through which people discovered state. It no longer owned a single invented state of its own.

Precise status was a consequence of precise identity: every transition could report what it changed and what it left untouched.

The chain prepared later breakthroughs

Once R7K1 separated commit, build inputs, artifact, environment, and route, other problems became tractable.

Content-addressed manifests could safely reuse artifacts. Preview isolation policy attached to environments. Expiry applied to running instances and routes without deleting source evidence. Readiness gated activation rather than build success. A route could roll back to a prior verified environment.

The work also revealed where receipts belonged:

build receipt: inputs -> artifact
provision receipt: artifact + runtime inputs -> environment
activation receipt: route generation -> environment

Each transition could be retried under stable identity. A timeout no longer required guessing whether a branch “had deployed.”

The branch-name failure was small and foundational. It forced the preview system to acquire a real state model.

The friendly branch route always selected one active generation. Reviewers also needed to compare the previous candidate without racing the route pointer.

Each ready environment received an immutable generation URL scoped to its environment ID. The branch page linked “current,” “previous verified,” and any pinned candidate. Opening two generation URLs guaranteed that each remained tied to its original artifact and fixture for its retention window.

This made visual and behavioral comparison accountable. A screenshot recorded the generation URL and build identity. A comment on the older candidate did not become a statement about the branch after it advanced.

Generation URLs were protected by the same access policy and expired with their environments. They did not promise permanent hosting; the review record retained manifests after runtime removal.

The branch URL optimized discovery. Generation URLs optimized evidence. Trying to make one mutable address serve both purposes had caused the original confusion.

A moving name became a safe doorway

R7K1 never stopped using branch preview URLs. They were the feature reviewers loved.

The change was what the URL meant. It became a mutable doorway to the latest verified environment accepted for one proposal. The response named the immutable source, artifact, and environment behind it. Historical generations had their own URLs and evidence.

A branch could move, be rewritten, or disappear. Existing artifacts did not change contents. Running environments did not change origin. Review results did not float to the new head. Route activation followed generation authority instead of completion time.

The lesson generalized: a friendly moving name is useful when the system admits it is moving. Problems begin when that name is asked to be source revision, artifact identity, deployment record, and runtime instance all at once.

A branch is a pointer. R7K1 became dependable when it stopped treating the pointer as the thing.