Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ node_modules
**/.next
**/.turbo
**/dist
**/*.tsbuildinfo
.env
.env.local
.env*.local
Expand Down
64 changes: 64 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Docker

on:
workflow_dispatch:
inputs:
version:
description: "Existing editor release version (without the package prefix)"
required: true
type: string
publish:
description: "Publish to GHCR (otherwise build only)"
default: false
type: boolean

concurrency:
group: docker-publication
cancel-in-progress: false

jobs:
image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Validate version
env:
VERSION: ${{ inputs.version }}
run: |
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$ ]] || [[ ${#VERSION} -gt 128 ]]; then
echo 'Expected an editor release version suitable for a Docker tag.' >&2
exit 1
fi

- uses: actions/checkout@v4
with:
ref: refs/tags/@pascal-app/editor@${{ inputs.version }}
persist-credentials: false

- name: Verify release version
env:
VERSION: ${{ inputs.version }}
run: jq -e --arg version "$VERSION" '.version == $version' packages/editor/package.json

- uses: docker/setup-qemu-action@v3

- uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
if: inputs.publish
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build versioned image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ inputs.publish }}
tags: ghcr.io/${{ github.repository }}:${{ inputs.version }}
labels: org.opencontainers.image.source=https://github.com/${{ github.repository }}
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Matches `packageManager` in package.json and the version CI installs — a skew
# here is what makes `--frozen-lockfile` fail inside the image but not locally.
FROM oven/bun:1.3.14-alpine
LABEL org.opencontainers.image.source="https://github.com/pascalorg/editor"
WORKDIR /app

# `next build` runs under `node`, and this image's `node` is a shim that re-execs
Expand Down
38 changes: 36 additions & 2 deletions SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ Local development and the official hosted editor work without any environment va
docker compose up -d
```

The editor will be running at **http://localhost:3000**. Saved scenes live in
the `pascal-data` volume, so they survive `docker compose down`.
The editor will be running at **http://localhost:3000**. Compose builds from source;
it does not depend on a GHCR package being available. Saved scenes live in the
`pascal-data` volume, so they survive `docker compose down`.

Docker defaults `MINT_PASCAL_HOST_ORIGIN` to `http://localhost:3000`. Override
it when hosting Pascal at another origin:
Expand All @@ -54,6 +55,39 @@ a base URL that only `NEXT_PUBLIC_APP_URL` can override, and Next inlines that
value at build time, so remapping the port to something else makes the page
return 500.

### GHCR bootstrap (maintainers)

Docker publication is separate from the npm release workflow. After a successful
editor release, run the **Docker** workflow with the existing version (for example,
`1.0.0-beta.5`). It checks out `@pascal-app/editor@<version>` and builds amd64 and
arm64 images. Leave `publish` disabled for a build-only check, then enable it to
publish `ghcr.io/<owner>/<repository>:<version>`. It never updates `latest`.

Before switching the default Compose configuration in a follow-up PR:

1. Publish a version from a completed editor release.
2. In the organization's Packages settings, connect the package to `pascalorg/editor`
and set its visibility to **Public** (a one-time maintainer action).
3. Using a Docker configuration without registry credentials, inspect the manifest
and pull both platforms. Replace `<version>` with the published version:

```bash
docker buildx imagetools inspect ghcr.io/pascalorg/editor:<version>
docker pull --platform linux/amd64 ghcr.io/pascalorg/editor:<version>
docker pull --platform linux/arm64 ghcr.io/pascalorg/editor:<version>
```

4. Smoke-test startup and scene persistence on both architectures and record the
version, digest, and results in the follow-up PR. The maintainer must approve
promotion to `latest` and the default-Compose transition separately.

If a Docker build or push fails, npm releases and source-based Compose remain
unchanged. Inspect GHCR for a partially uploaded version before retrying the same
release tag; do not rerun npm publishing or move the Git tag. For a bad image, keep
users on the source build or a previously verified image digest, and publish a
corrected release version rather than silently replacing an image users may have
pinned. Registry cleanup and any future `latest` rollback require maintainer approval.

## CLI-managed editor

Node.js 22.13 or newer can install a persistent local runtime, start it in the
Expand Down
31 changes: 31 additions & 0 deletions scripts/docker-workflow.test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require 'yaml'
require 'open3'

Dir.chdir(File.expand_path('..', __dir__)) do
workflow = YAML.load_file('.github/workflows/docker.yml')
steps = workflow.fetch('jobs').fetch('image').fetch('steps')
validation = steps.find { |step| step['name'] == 'Validate version' }.fetch('run')
{
'1.0.0' => true,
'1.0.0-beta.5' => true,
'main' => false,
'../main' => false,
'1.0.0;echo bad' => false,
'' => false,
"1.0.0-#{'a' * 128}" => false
}.each do |version, expected|
_, status = Open3.capture2e({'VERSION' => version}, 'bash', '-e', '-c', validation)
raise "Incorrect version validation: #{version}" unless status.success? == expected
end

build = steps.find { |step| step['uses'] == 'docker/build-push-action@v6' }.fetch('with')
raise 'Missing architecture' unless build['platforms'] == 'linux/amd64,linux/arm64'
raise 'Publication must be opt-in' unless build['push'] == '${{ inputs.publish }}'
raise 'Do not promote latest during bootstrap' if build['tags'].include?('latest')
checkout = steps.find { |step| step['uses'] == 'actions/checkout@v4' }.fetch('with')
raise 'Build a release tag' unless checkout['ref'] == 'refs/tags/@pascal-app/editor@${{ inputs.version }}'
compose = YAML.load_file('docker-compose.yml').fetch('services').fetch('editor')
raise 'Keep source builds working' unless compose['build'] == '.' && !compose.key?('image')
raise 'Keep npm releases independent' if File.read('.github/workflows/release.yml').include?('docker/')
puts 'PASS: Docker bootstrap workflow checks'
end