Skip to content

chore(ci): build and publish the Stacklet cubejs/cube image - #2

Merged
jtroup merged 4 commits into
stacklet/integrationfrom
elmo/duly-valid-guppy
Oct 23, 2026
Merged

jtroup merged 4 commits into
stacklet/integrationfrom
elmo/duly-valid-guppy

Conversation

@jtroup

@jtroup jtroup commented Oct 23, 2026 •

Copy link
Copy Markdown

ENG-7751

what

Add a Stacklet Image workflow that builds the Stacklet
cubejs/cube image with scripts/build-docker.sh and pushes it to
ECR. Pushes to stacklet/integration and release/** publish
v<version>-stacklet-<sha9>. Same-repo PRs publish
v<version>-stacklet-pr<N>-<sha9>. Fork PRs do not run.

The workflow has four jobs:

  1. "Overlay guard" runs the new scripts/stacklet-overlay-guard.sh.
    The script fails on any change under packages/ or rust/ that
    does not reach the image. It allows Markdown, tests,
    packages/cubejs-docker/, and the TypeScript sources of patched
    packages. It compares against the upstream release tag that
    matches packages/cubejs-docker/package.json.
  2. "Resolve image" computes the tag and checks whether it exists in
    ECR.
  3. "Build" builds a single-platform amd64 image and checks that the
    logger patch is present. It then starts the container, waits for
    /livez, and saves the image as an artifact. It does not run when
    the tag exists.
  4. "Publish" pushes the image and checks that ECR holds a
    single-platform manifest for the tag.

Only "Build" runs third-party code, and it has no cloud credentials
or OIDC token. The role, region and registry come from repo Actions
variables.

scripts/build-docker.sh now reads the overlaid packages from one
PATCHED_PACKAGES list. It compiles with yarn tsc, copies the root
lockfile into the Docker build context, and passes extra arguments to
docker build.

The overlay step in latest.Dockerfile now replaces each patched
package's dist/ instead of copying over it, so a file deleted from
the source does not survive from the npm build. It also fails the
build when a staged package is not installed or a copy fails.

A Stacklet Lint workflow runs pre-commit with actionlint, zizmor
and shellcheck. The config limits the hooks to the Stacklet workflows
and scripts, so upstream files stay as upstream has them. mise.toml
pins pre-commit for the workflow.

Also add .github/CODEOWNERS and trim the Stacklet section of
packages/cubejs-docker/DEVELOPMENT.md.

why

A CI build ties each published image to one commit and a clean
checkout.

The image installs npm packages and overlays only the compiled output
of packages in PATCHED_PACKAGES. A source change to any other
package does not reach the image. The guard makes that a CI failure.

The upstream workflows stay as they are, so merges of upstream
release tags do not conflict. We disable them in the repository
settings instead.

testing

A local build with the workflow's arguments passes the smoke test.
Its npm packages and compiled server-core files match the current
published image. The only package differences are newer Debian
security updates from the base image.

This PR's own run of the workflow tests the ECR authentication and
push.

docs

packages/cubejs-docker/DEVELOPMENT.md covers the build script and
the guard. No other docs needed.


🤖 Generated with Claude Code

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 9 files

Confidence score: 2/5

  • In .github/workflows/stacklet-image.yml, same-repository pull requests run the PR’s workflow revision, which can request OIDC credentials for the ECR publishing role. Keep AWS-authenticated publishing out of pull request runs.
  • In scripts/stacklet-overlay-guard.sh, root-level files such as rust/.gitignore bypass the dropped-path check, so changes that do not reach the image can pass the guard. Match both root-level and nested Rust paths.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".github/workflows/stacklet-image.yml">

<violation number="1" location=".github/workflows/stacklet-image.yml:84">
P1: Custom agent: **Flag Security Vulnerabilities**

Same-repository `pull_request` runs execute the PR’s workflow revision, and this job can request OIDC credentials for the ECR publishing role. Keep AWS-authenticated publication to trusted push runs or require an explicit trusted approval gate for PRs.</violation>
</file>

<file name="scripts/stacklet-overlay-guard.sh">

<violation number="1" location="scripts/stacklet-overlay-guard.sh:74">
P2: Root-level files under `rust/` bypass the dropped-path check, so a change to `rust/.gitignore` is silently accepted despite not reaching the image. Match both `rust/<file>` and nested Rust paths.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Dev as Developer
    participant Git as GitHub Repo
    participant WF as Stacklet Image Workflow
    participant Node as Node.js Build Env
    participant Docker as Docker Build/Runtime
    participant ECR as AWS ECR Registry
    participant AWS as AWS OIDC Auth

    Note over Dev,Git: PR or push triggers workflow
    Dev->>Git: Push to stacklet/integration or release/* branch
    Git->>WF: Trigger Stacklet Image workflow

    Note over WF: Job 1: Overlay Guard
    WF->>Node: Run stacklet-overlay-guard.sh
    Node->>Git: Fetch upstream release tag(s)
    Git-->>Node: Upstream tag refs
    Node->>Node: Compare changed files vs upstream
    alt Unreachable source changes detected
        Node-->>WF: FAIL - changes not in image
    else Overlay passed
        Node-->>WF: Overlay OK
    end

    Note over WF: Job 2: Resolve Image Tag
    WF->>AWS: Assume publish role (OIDC)
    AWS-->>WF: Temporary credentials
    WF->>Node: Resolve tag from package.json + commit sha
    Node-->>WF: Image tag (e.g. v1.2.3-stacklet-pr42-abc123def)
    WF->>ECR: Check if tag already exists
    alt Tag exists
        ECR-->>WF: Exists - skip build
    else Tag missing
        ECR-->>WF: Not found - proceed to build
    end

    Note over WF: Job 3: Build (no cloud creds)
    WF->>Node: yarn install + tsc compile (PATCHED_PACKAGES list)
    Node->>Node: Copy root yarn.lock into build context
    Node->>Docker: docker build --platform linux/amd64
    Docker->>Node: Install npm packages + overlay compiled dist
    alt Patch overlay fails
        Node-->>WF: FAIL - package not installed or copy error
    else Build succeeds
        Docker-->>Node: Image built
        Node->>Docker: Run container, probe /livez
        alt Health check fails
            Docker-->>WF: FAIL - smoke test error
        else Health check passes
            Docker-->>Node: Save image artifact
        end
    end

    Note over WF: Job 4: Publish
    WF->>AWS: Assume publish role (OIDC)
    AWS-->>WF: Temporary credentials
    WF->>ECR: Push image to ECR repository
    ECR-->>WF: Image pushed
    WF->>ECR: Validate single-platform manifest
    ECR-->>WF: Manifest verified
    WF-->>Git: Workflow complete

    Note over Dev,Git: Final state: image published and tagged in ECR
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

if: vars.PUBLISH_ROLE_ARN != ''
permissions:
contents: read
id-token: write

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag Security Vulnerabilities

Same-repository pull_request runs execute the PR’s workflow revision, and this job can request OIDC credentials for the ECR publishing role. Keep AWS-authenticated publication to trusted push runs or require an explicit trusted approval gate for PRs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .github/workflows/stacklet-image.yml, line 84:

<comment>Same-repository `pull_request` runs execute the PR’s workflow revision, and this job can request OIDC credentials for the ECR publishing role. Keep AWS-authenticated publication to trusted push runs or require an explicit trusted approval gate for PRs.</comment>

<file context>
@@ -0,0 +1,332 @@
+    if: vars.PUBLISH_ROLE_ARN != ''
+    permissions:
+      contents: read
+      id-token: write
+    outputs:
+      sha: ${{ steps.meta.outputs.sha }}
</file context>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberate. Same-repo PR images are intended (-pr<N>- tags for pre-merge testing). PR creation is limited to collaborators, the ECR repo is immutable so a PR cannot overwrite an existing tag, and branch protection (PRs required, admins included, no rebase-merge) stops a PR head SHA from becoming a published branch SHA. The remaining risk is a collaborator creating junk tags, which we accept.


🤖 Generated with Claude Code

Comment thread scripts/stacklet-overlay-guard.sh Outdated
if [[ "$path" =~ ^(packages/[^/]+)/([^/]+)/ && "${shipped[${BASH_REMATCH[1]}]:-}" = "${BASH_REMATCH[2]}" ]]; then
continue
fi
if [[ "$path" =~ ^((packages|rust)/[^/]+)/ ]]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Root-level files under rust/ bypass the dropped-path check, so a change to rust/.gitignore is silently accepted despite not reaching the image. Match both rust/<file> and nested Rust paths.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At scripts/stacklet-overlay-guard.sh, line 74:

<comment>Root-level files under `rust/` bypass the dropped-path check, so a change to `rust/.gitignore` is silently accepted despite not reaching the image. Match both `rust/<file>` and nested Rust paths.</comment>

<file context>
@@ -0,0 +1,90 @@
+  if [[ "$path" =~ ^(packages/[^/]+)/([^/]+)/ && "${shipped[${BASH_REMATCH[1]}]:-}" = "${BASH_REMATCH[2]}" ]]; then
+    continue
+  fi
+  if [[ "$path" =~ ^((packages|rust)/[^/]+)/ ]]; then
+    dropped[${BASH_REMATCH[1]}]+="    $path"$'\n'
+  fi
</file context>
Suggested change
if [[ "$path" =~ ^((packages|rust)/[^/]+)/ ]]; then
if [[ "$path" =~ ^((packages|rust)(/[^/]+)?)/ ]]; then

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applying. The only file directly under rust/ is .gitignore, which is repo config, not code that ships, the same as the root-level files the guard intentionally ignores. The guard targets code changes inside packages and crates that would silently not reach the image.


🤖 Generated with Claude Code

@jtroup

jtroup commented Oct 23, 2026

Copy link
Copy Markdown
Author

@cubic-ai

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 23, 2026

Copy link
Copy Markdown

@cubic-ai

@jtroup I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 9 files

Architecture diagram
sequenceDiagram
    participant GH as GitHub Events
    participant WF as Stacklet Image Workflow
    participant OG as Overlay Guard Job
    participant RS as Resolve Job
    participant BD as Build Job
    participant PB as Publish Job
    participant ECR as AWS ECR
    participant BS as Build Script
    participant DK as Docker Build
    participant IMG as Cube Image

    Note over GH,ECR: CI/CD Pipeline for Stacklet Cube Image

    GH->>WF: push (stacklet/integration, release/*) or PR
    WF->>WF: Check event type and repo

    alt Fork PR
        WF->>WF: Skip workflow (no run)
    else Same-repo PR or push
        WF->>OG: Start overlay guard
        OG->>OG: Fetch upstream release tag
        OG->>OG: Check changed files under packages/ and rust/
        alt Changes reach image
            OG-->>WF: OK
        else Changes don't reach image (e.g., Rust, non-patched)
            OG-->>WF: FAIL (block workflow)
        end

        WF->>RS: Resolve image tag and existence
        RS->>RS: Compute tag: v<version>-stacklet-<sha9> or v<version>-stacklet-pr<N>-<sha9>
        RS->>ECR: Check if image tag exists
        alt Tag exists
            ECR-->>RS: Image found
            RS-->>WF: exists=true (skip build)
        else Tag not found
            ECR-->>RS: ImageNotFoundException
            RS-->>WF: exists=false
        end

        alt exists=false
            WF->>BD: Build image
            BD->>BS: Run build-docker.sh with image tag
            BS->>BS: Read PATCHED_PACKAGES list
            BS->>BS: Compile TypeScript (yarn tsc)
            BS->>BS: Stage patched package dist to stacklet-patches/
            BS->>BS: Copy root yarn.lock to Docker context
            BS->>DK: docker build (platform: amd64, no provenance)
            DK->>DK: Overlay patched packages from stacklet-patches/
            alt Overlay target not installed
                DK->>DK: FAIL build (explicit error)
            else Copy failure
                DK->>DK: FAIL build
            end
            DK-->>BS: Image built
            BS-->>BD: Image ready
            BD->>IMG: Start container (smoke test)
            IMG-->>BD: /livez returns 200
            BD->>BD: Save image as gzip artifact
            BD-->>WF: Upload artifact
        end

        WF->>PB: Publish (uses artifact or existing image)
        PB->>ECR: Push image to registry
        ECR-->>PB: Push successful
        PB->>ECR: Verify single-platform manifest
        ECR-->>PB: Manifest confirmed
        PB-->>WF: Complete
    end

    Note over WF: Only Build job runs third-party code (yarn, docker).<br/>No cloud credentials or OIDC token in Build job.
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread scripts/stacklet-overlay-guard.sh
Comment thread scripts/stacklet-overlay-guard.sh

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread .github/workflows/stacklet-image.yml
Comment thread .github/workflows/stacklet-image.yml
Comment thread .github/workflows/stacklet-image.yml
Comment thread .github/workflows/stacklet-image.yml
Comment thread .github/workflows/stacklet-image.yml
@jtroup
jtroup merged commit b124da6 into stacklet/integration Oct 23, 2026
7 checks passed
@jtroup
jtroup deleted the elmo/duly-valid-guppy branch October 23, 2026 16:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants