Skip to content

Commit c45ce08

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 c45ce08

2 files changed

Lines changed: 60 additions & 13 deletions

File tree

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

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ on:
99
description: 'Override Studio version hash (leave empty for latest)'
1010
required: false
1111
push:
12-
branches: [main]
12+
branches: [main, 'feat/**']
1313
paths:
1414
- 'tools/studio-bridge/docker/**'
1515
- '.github/workflows/studio-linux-docker-build.yml'
@@ -24,6 +24,8 @@ jobs:
2424
outputs:
2525
version: ${{ steps.resolve.outputs.version }}
2626
short: ${{ steps.resolve.outputs.short }}
27+
is_canary: ${{ steps.resolve.outputs.is_canary }}
28+
canary_tag: ${{ steps.resolve.outputs.canary_tag }}
2729
steps:
2830
- name: Resolve Studio version
2931
id: resolve
@@ -37,6 +39,19 @@ jobs:
3739
echo "short=${VERSION:0:12}" >> "$GITHUB_OUTPUT"
3840
echo "Resolved Studio version: $VERSION"
3941
42+
# Canary builds for non-main branches
43+
if [ "${{ github.ref_name }}" != "main" ]; then
44+
BRANCH="${{ github.ref_name }}"
45+
# Sanitize branch name for Docker tag (replace / with -)
46+
CANARY_TAG="canary-${BRANCH//\//-}"
47+
echo "is_canary=true" >> "$GITHUB_OUTPUT"
48+
echo "canary_tag=$CANARY_TAG" >> "$GITHUB_OUTPUT"
49+
echo "Canary build: $CANARY_TAG"
50+
else
51+
echo "is_canary=false" >> "$GITHUB_OUTPUT"
52+
echo "canary_tag=" >> "$GITHUB_OUTPUT"
53+
fi
54+
4055
check-existing:
4156
needs: resolve-version
4257
runs-on: ubuntu-latest
@@ -46,6 +61,13 @@ jobs:
4661
- name: Check if image already exists
4762
id: check
4863
run: |
64+
# Always rebuild canary images
65+
if [ "${{ needs.resolve-version.outputs.is_canary }}" = "true" ]; then
66+
echo "exists=false" >> "$GITHUB_OUTPUT"
67+
echo "Canary build — always rebuild"
68+
exit 0
69+
fi
70+
4971
if docker manifest inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.resolve-version.outputs.version }} > /dev/null 2>&1; then
5072
echo "exists=true" >> "$GITHUB_OUTPUT"
5173
echo "Image already exists for version ${{ needs.resolve-version.outputs.version }}, skipping build"
@@ -77,6 +99,23 @@ jobs:
7799
username: ${{ github.actor }}
78100
password: ${{ secrets.GITHUB_TOKEN }}
79101

102+
- name: Compute image tags
103+
id: tags
104+
run: |
105+
if [ "${{ needs.resolve-version.outputs.is_canary }}" = "true" ]; then
106+
# Canary: only the canary tag, never :latest
107+
TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.resolve-version.outputs.canary_tag }}"
108+
else
109+
# Production: latest + version tags
110+
TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
111+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.resolve-version.outputs.version }}
112+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.resolve-version.outputs.short }}"
113+
fi
114+
# Use EOF delimiter for multiline
115+
echo "tags<<ENDOFTAGS" >> "$GITHUB_OUTPUT"
116+
echo "$TAGS" >> "$GITHUB_OUTPUT"
117+
echo "ENDOFTAGS" >> "$GITHUB_OUTPUT"
118+
80119
- name: Build and push image
81120
uses: docker/build-push-action@v6
82121
with:
@@ -85,10 +124,7 @@ jobs:
85124
build-args: |
86125
STUDIO_VERSION=${{ needs.resolve-version.outputs.version }}
87126
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 }}
127+
tags: ${{ steps.tags.outputs.tags }}
92128
cache-from: type=gha
93129
cache-to: type=gha,mode=max
94130

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)