Skip to content

Commit 23c26b0

Browse files
committed
feat(studio-bridge): canary Docker builds for feature branches
Non-main branches now build canary images tagged as canary-<branch-name> instead of :latest. Use STUDIO_BRIDGE_DOCKER_TAG=canary-feat-my-branch to test with a canary image locally.
1 parent fac635c commit 23c26b0

2 files changed

Lines changed: 64 additions & 12 deletions

File tree

.github/workflows/studio-linux-docker-build.yml

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ on:
1313
paths:
1414
- 'tools/studio-bridge/docker/**'
1515
- '.github/workflows/studio-linux-docker-build.yml'
16+
pull_request:
17+
paths:
18+
- 'tools/studio-bridge/docker/**'
19+
- '.github/workflows/studio-linux-docker-build.yml'
1620

1721
env:
1822
REGISTRY: ghcr.io
@@ -24,6 +28,8 @@ jobs:
2428
outputs:
2529
version: ${{ steps.resolve.outputs.version }}
2630
short: ${{ steps.resolve.outputs.short }}
31+
is_canary: ${{ steps.resolve.outputs.is_canary }}
32+
canary_tag: ${{ steps.resolve.outputs.canary_tag }}
2733
steps:
2834
- name: Resolve Studio version
2935
id: resolve
@@ -37,6 +43,20 @@ jobs:
3743
echo "short=${VERSION:0:12}" >> "$GITHUB_OUTPUT"
3844
echo "Resolved Studio version: $VERSION"
3945
46+
# Canary builds for PRs and non-main branches
47+
# For pull_request events, head_ref is the source branch; for push, ref_name is the branch
48+
BRANCH="${{ github.head_ref || github.ref_name }}"
49+
if [ "$BRANCH" != "main" ]; then
50+
# Sanitize branch name for Docker tag (replace / with -)
51+
CANARY_TAG="canary-${BRANCH//\//-}"
52+
echo "is_canary=true" >> "$GITHUB_OUTPUT"
53+
echo "canary_tag=$CANARY_TAG" >> "$GITHUB_OUTPUT"
54+
echo "Canary build: $CANARY_TAG"
55+
else
56+
echo "is_canary=false" >> "$GITHUB_OUTPUT"
57+
echo "canary_tag=" >> "$GITHUB_OUTPUT"
58+
fi
59+
4060
check-existing:
4161
needs: resolve-version
4262
runs-on: ubuntu-latest
@@ -46,6 +66,13 @@ jobs:
4666
- name: Check if image already exists
4767
id: check
4868
run: |
69+
# Always rebuild canary images
70+
if [ "${{ needs.resolve-version.outputs.is_canary }}" = "true" ]; then
71+
echo "exists=false" >> "$GITHUB_OUTPUT"
72+
echo "Canary build — always rebuild"
73+
exit 0
74+
fi
75+
4976
if docker manifest inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.resolve-version.outputs.version }} > /dev/null 2>&1; then
5077
echo "exists=true" >> "$GITHUB_OUTPUT"
5178
echo "Image already exists for version ${{ needs.resolve-version.outputs.version }}, skipping build"
@@ -77,6 +104,23 @@ jobs:
77104
username: ${{ github.actor }}
78105
password: ${{ secrets.GITHUB_TOKEN }}
79106

107+
- name: Compute image tags
108+
id: tags
109+
run: |
110+
if [ "${{ needs.resolve-version.outputs.is_canary }}" = "true" ]; then
111+
# Canary: only the canary tag, never :latest
112+
TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.resolve-version.outputs.canary_tag }}"
113+
else
114+
# Production: latest + version tags
115+
TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
116+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.resolve-version.outputs.version }}
117+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.resolve-version.outputs.short }}"
118+
fi
119+
# Use EOF delimiter for multiline
120+
echo "tags<<ENDOFTAGS" >> "$GITHUB_OUTPUT"
121+
echo "$TAGS" >> "$GITHUB_OUTPUT"
122+
echo "ENDOFTAGS" >> "$GITHUB_OUTPUT"
123+
80124
- name: Build and push image
81125
uses: docker/build-push-action@v6
82126
with:
@@ -85,10 +129,7 @@ jobs:
85129
build-args: |
86130
STUDIO_VERSION=${{ needs.resolve-version.outputs.version }}
87131
push: true
88-
tags: |
89-
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
90-
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.resolve-version.outputs.version }}
91-
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.resolve-version.outputs.short }}
132+
tags: ${{ steps.tags.outputs.tags }}
92133
cache-from: type=gha
93134
cache-to: type=gha,mode=max
94135

tools/studio-bridge/src/docker/docker-delegator.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,18 @@ import { OutputHelper } from '@quenty/cli-output-helpers';
1212
import { validateCookieAsync } from '@quenty/nevermore-cli-helpers';
1313
import type { ExecuteScriptOptions } from '../cli/script-executor.js';
1414

15-
const DOCKER_IMAGE = 'ghcr.io/quenty/nevermore-studio-linux:latest';
15+
const DOCKER_IMAGE_BASE = 'ghcr.io/quenty/nevermore-studio-linux';
1616
const CHECK_TIMEOUT_MS = 5_000;
1717

18+
/**
19+
* Resolves the Docker image to use. Defaults to :latest, but can be
20+
* overridden with STUDIO_BRIDGE_DOCKER_TAG (e.g. "canary-feat-my-branch").
21+
*/
22+
function resolveDockerImage(): string {
23+
const tag = process.env.STUDIO_BRIDGE_DOCKER_TAG ?? 'latest';
24+
return `${DOCKER_IMAGE_BASE}:${tag}`;
25+
}
26+
1827
/**
1928
* Returns true if the current environment should delegate to Docker
2029
* (Linux without Wine, but with Docker available).
@@ -58,10 +67,11 @@ export async function delegateToDockerAsync(
5867

5968
await validateCookieAsync(cookie);
6069

61-
await ensureImageAsync();
70+
const image = resolveDockerImage();
71+
await ensureImageAsync(image);
6272

6373
const cwd = process.cwd();
64-
const args = await buildDockerRunArgsAsync(options, cwd, cookie);
74+
const args = await buildDockerRunArgsAsync(options, cwd, cookie, image);
6575

6676
// Log args without the cookie value
6777
const safeArgs = args.map(a =>
@@ -80,14 +90,14 @@ export async function delegateToDockerAsync(
8090
/**
8191
* Ensures the Docker image is available locally, pulling if needed.
8292
*/
83-
async function ensureImageAsync(): Promise<void> {
93+
async function ensureImageAsync(image: string): Promise<void> {
8494
try {
85-
await execa('docker', ['image', 'inspect', DOCKER_IMAGE], {
95+
await execa('docker', ['image', 'inspect', image], {
8696
stdio: 'ignore',
8797
});
8898
} catch {
89-
OutputHelper.info(`Pulling ${DOCKER_IMAGE}...`);
90-
await execa('docker', ['pull', DOCKER_IMAGE], { stdio: 'inherit' });
99+
OutputHelper.info(`Pulling ${image}...`);
100+
await execa('docker', ['pull', image], { stdio: 'inherit' });
91101
}
92102
}
93103

@@ -99,6 +109,7 @@ export async function buildDockerRunArgsAsync(
99109
options: ExecuteScriptOptions,
100110
cwd: string,
101111
cookie: string,
112+
image: string = `${DOCKER_IMAGE_BASE}:latest`,
102113
): Promise<string[]> {
103114
const { scriptContent, placePath, timeoutMs, verbose } = options;
104115

@@ -165,7 +176,7 @@ export async function buildDockerRunArgsAsync(
165176
'-e', `ROBLOSECURITY=${cookie}`,
166177
'-v', `${cwd}:${cwd}`,
167178
'-w', cwd,
168-
DOCKER_IMAGE,
179+
image,
169180
'bash', '-c', innerArgs.join(' '),
170181
];
171182
}

0 commit comments

Comments
 (0)