# Publishing Role Images (https://jackin.tailrocks.com/developing/publishing-role-images/)



<Aside type="note">
  This page is **for role authors** — anyone who needs to build and publish a Docker image from their role repository. If you are hosting your role on GitHub and use the standard `jackin-project/jackin-role-action` reusable workflow, most of this happens automatically. Read the [Using the provided workflow](#using-the-provided-workflow-recommended) section to confirm the setup, and the [Custom CI/CD](#custom-cicd) section only if you build outside GitHub Actions.
</Aside>

## Required OCI labels [#required-oci-labels]

Every published role image must carry two OCI labels that jackin reads at launch time:

| Label                      | Value                                                            | Purpose                                                                                                                                       |
| -------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `jackin.construct.version` | The version tag from the `FROM` line (e.g. `0.2`)                | Staleness detection — jackin warns when the published image was built from an older construct than the one currently resolved for a new build |
| `jackin.role.git.sha`      | The short (7-char) git commit SHA of the role repo at build time | Version display and staleness tracking — compared against the role checkout's HEAD                                                            |

<Aside type="caution">
  If either label is missing or set to `unknown`, jackin❯ treats the image as built without version tracking. Staleness warnings are suppressed — jackin❯ cannot detect that the published image is behind. The image still loads and runs, but the operator loses visibility into whether they are running a stale agent.
</Aside>

<Aside type="note">
  Keys use dotted namespacing (`jackin.construct.version`, `jackin.role.git.sha`). `jackin-role publish-labels --role-git-sha <sha>` prints this label set for workflows and custom CI so the runtime freshness contract stays in the binary instead of duplicated in YAML. Until your role re-publishes with the updated `jackin-role-action`, jackin falls back to a local workspace rebuild for that role (correct, just not the fast published path). See [Image Labels & Recipe Hash](/reference/runtime/image-labels/) for the full label schema jackin verifies.
</Aside>

## Using the provided workflow (recommended) [#using-the-provided-workflow-recommended]

Role repositories on GitHub should call the `jackin-project/jackin-role-action` reusable workflow. It handles label injection, Docker layer cache ordering, GitHub API authentication, multi-platform builds (linux/amd64 and linux/arm64), image signing, and manifest assembly automatically.

A minimal caller workflow in your role repo:

```yaml title=".github/workflows/publish-image.yml"
name: Publish Image

on:
  push:
    branches: [main]
    paths:
      - 'Dockerfile'
      - 'jackin.role.toml'
      - '.github/workflows/publish-image.yml'
  workflow_dispatch:

jobs:
  publish:
    uses: jackin-project/jackin-role-action/.github/workflows/publish.yml@<SHA> # pin to a release SHA
    permissions:
      contents: read
      id-token: write
    secrets:
      registry-username: ${{ secrets.DOCKERHUB_USERNAME }}
      registry-password: ${{ secrets.DOCKERHUB_TOKEN }}
      gh-readonly-token: ${{ secrets.GH_READONLY_TOKEN }}
```

Replace `<SHA>` with the pinned commit SHA from the [jackin-role-action releases](https://github.com/jackin-project/jackin-role-action). Renovate can keep the pin current automatically — the generated role repo scaffold includes a Renovate configuration with the right pattern.

**What the workflow does automatically:**

* Runs `jackin-role validate`, `jackin-role published-image-repository`, and `jackin-role publish-labels` to extract the image repository and label set from the role contract.
* Applies `jackin.construct.version` and `jackin.role.git.sha` as OCI labels at build time via `docker/build-push-action`'s `labels:` input — no `ARG` or `LABEL` declarations needed in your Dockerfile.
* Builds separate image manifests for `linux/amd64` and `linux/arm64`.
* Assembles and pushes a multi-platform manifest list.
* Signs the image with Sigstore cosign.

Your Dockerfile only needs to declare the tools your role installs. The `FROM projectjackin/construct:<version>-<distro>` line is the construct version source of truth — the workflow reads it automatically.

## Custom CI/CD [#custom-cicd]

If your role repo is not on GitHub, or if you use a different CI/CD system (GitLab CI, Jenkins, Buildkite, CircleCI, etc.), you must apply the two labels yourself at build time.

### Minimum required steps [#minimum-required-steps]

<Steps>
  1. **Determine the construct version**

     Read the version from your Dockerfile's `FROM` line. The version is the part between `:` and `-` in the construct tag:

     ```dockerfile
     FROM projectjackin/construct:0.4-trixie
     #                            ^^^— this is the construct version
     ```

     If the `jackin-role` binary is available in your CI environment (download it from the [jackin releases](https://github.com/jackin-project/jackin/releases)), prefer asking it for the complete label set:

     ```sh
     ROLE_GIT_SHA=$(git rev-parse HEAD)
     jackin-role publish-labels --role-git-sha "${ROLE_GIT_SHA}" .
     ```

     Otherwise, parse it directly from the `FROM` line:

     ```sh
     CONSTRUCT_VERSION=$(grep -m1 '^FROM projectjackin/construct:' Dockerfile \
       | sed 's|FROM projectjackin/construct:\([^-]*\)-.*|\1|')
     ```

  2. **Get the role commit SHA**

     ```sh
     ROLE_GIT_SHA=$(git rev-parse HEAD)
     ```

  3. **Build the image with both labels**

     Pass both labels using `--label` flags:

     ```sh
     CONSTRUCT_VERSION=$(jackin-role construct-version .)
     docker build \
       --label "jackin.construct.version=${CONSTRUCT_VERSION}" \
       --label "jackin.role.git.sha=${ROLE_GIT_SHA}" \
       -t your-registry/your-role:latest \
       .
     ```

     No `ARG` or `LABEL` declarations are needed in the Dockerfile.
</Steps>

### Layer cache ordering [#layer-cache-ordering]

`ARG` instructions in a Dockerfile invalidate all subsequent layers when the argument value changes. A git SHA changes on every commit — so if you declare `ARG ROLE_GIT_SHA` inside the Dockerfile, every commit busts the layer cache for everything below it.

Using `--label` at build time (or the `labels:` input in `docker/build-push-action`) bypasses this: the label is applied to the final image manifest without touching the Dockerfile instruction stream. Your expensive tool-installation `RUN` layers remain cached across commits.

<Aside type="tip">
  If you are migrating from an older role repo that has `ARG CONSTRUCT_VERSION`/`LABEL jackin.construct.version` and `ARG ROLE_GIT_SHA`/`LABEL jackin.role.git.sha` in the Dockerfile, remove those declarations and switch to `--label` flags at build time. The `FROM` line is still how jackin reads the construct version — it does not need to be re-declared as an `ARG`.
</Aside>

### Example: GitLab CI [#example-gitlab-ci]

```yaml title=".gitlab-ci.yml"
build-and-push:
  stage: build
  image: docker:27
  services:
    - docker:27-dind
  script:
    - CONSTRUCT_VERSION=$(grep -m1 '^FROM projectjackin/construct:' Dockerfile | sed 's|FROM projectjackin/construct:\([^-]*\)-.*|\1|')
    - ROLE_GIT_SHA=${CI_COMMIT_SHA}
    - docker login -u "${REGISTRY_USERNAME}" -p "${REGISTRY_PASSWORD}"
    - >
      docker build
      --label "jackin.construct.version=${CONSTRUCT_VERSION}"
      --label "jackin.role.git.sha=${ROLE_GIT_SHA}"
      -t "${REGISTRY_IMAGE}:latest"
      .
    - docker push "${REGISTRY_IMAGE}:latest"
```

## Using a different registry [#using-a-different-registry]

The reusable workflow accepts a `registry` input (defaults to Docker Hub) and matching `registry-username` / `registry-password` secrets. To publish to a different registry, pass the appropriate values:

<Tabs>
  <TabItem label="GHCR">
    ```yaml
    jobs:
      publish:
        uses: jackin-project/jackin-role-action/.github/workflows/publish.yml@<SHA>
        permissions:
          contents: read
          id-token: write
          packages: write
        secrets:
          registry-username: ${{ github.actor }}
          registry-password: ${{ secrets.GITHUB_TOKEN }}
    ```

    Set your `jackin.role.toml` `published_image` to the full GHCR path:

    ```toml title="jackin.role.toml"
    published_image = "ghcr.io/your-org/jackin-your-role"
    ```
  </TabItem>

  <TabItem label="ECR (AWS)">
    ```yaml
    jobs:
      publish:
        uses: jackin-project/jackin-role-action/.github/workflows/publish.yml@<SHA>
        permissions:
          contents: read
          id-token: write
        secrets:
          registry: 123456789.dkr.ecr.us-east-1.amazonaws.com
          registry-username: AWS
          registry-password: ${{ steps.ecr-login.outputs.token }}
    ```

    Obtain the ECR token in a preceding step using the AWS CLI or the `aws-actions/amazon-ecr-login` action, then pass it as `registry-password`. Set `published_image` in your manifest to the full ECR URI.
  </TabItem>

  <TabItem label="Custom registry">
    ```yaml
    jobs:
      publish:
        uses: jackin-project/jackin-role-action/.github/workflows/publish.yml@<SHA>
        with:
          registry: registry.your-org.internal
        secrets:
          registry-username: ${{ secrets.REGISTRY_USER }}
          registry-password: ${{ secrets.REGISTRY_PASS }}
    ```
  </TabItem>
</Tabs>

**jackin❯ must be able to pull the image at launch time.** If you publish to a private registry, the credentials must be available in the operator's Docker environment before running `jackin load` or `jackin console`. Configure Docker credentials on the operator's machine using `docker login <registry>` or a credential helper.

## Minimum Dockerfile requirements [#minimum-dockerfile-requirements]

* The final stage **must** start from `projectjackin/construct:<version>-<distro>` (see [Role Repositories](/guides/role-repos/#the-repo-contract) for the full contract).
* No `ARG CONSTRUCT_VERSION` or `ARG ROLE_GIT_SHA` declarations are needed. The workflow or `--label` flags at build time handle both.
* The construct version jackin reads for staleness detection comes from the `FROM` line — not from a label or ARG. Keep the version tag in the `FROM` line accurate and let Renovate track updates.
