Skip to content

ci(deploy): deploy pre-built images tagged by commit SHA - #2131

Open
alanpeixinho wants to merge 7 commits into
kernelci:mainfrom
profusion:chore/deploy-sha-images
Open

alanpeixinho wants to merge 7 commits into
kernelci:mainfrom
profusion:chore/deploy-sha-images

Conversation

@alanpeixinho

@alanpeixinho alanpeixinho commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

What it is

Staging and production deploy pre-built GHCR images by commit SHA instead of :latest or on-server builds.

  • Publish GHCR Images — Pick the branch or tag with Use workflow from. Images are tagged with that commit's full SHA. :latest only on push to main.
  • Staging — CI deploys the push SHA after tests. A manual run deploys the selected branch or tag. Pull + up --no-build.
  • Production — Manual run from a branch or tag. Pulls that commit's full SHA. DASHBOARD_VERSION comes from that commit.
  • Staging compose image paths match .env.example; docs updated.
    Do not run Deploy production Dashboard for this PR.

How to test

The workflow file has to be on kernelci/dashboard. A fork branch is not a valid --ref there. Push this commit to a branch on that repo, then pass that branch name.

1. Publish (no staging, no prod)

gh workflow run "Publish GHCR Images" \
  --repo kernelci/dashboard \
  --ref <branch>

There is no ref input. The branch you pass is the commit that gets built.

  1. Open the run → Summary: Ref: <branch> and Commit SHA: <full sha>.
  2. Confirm tags in GHCR (https://github.com/orgs/kernelci/packages):
    • dashboard-backend, dashboard-frontend, dashboard-proxy each have a tag equal to that full SHA.
    • On this manual run, the latest tag digest should be unchanged.

2. Negative staging (unpublished SHA)

Use a real branch whose commit was not published. A fake SHA cannot be passed; --ref must exist or the run never starts.

gh workflow run "Deploy Staging and E2E" \
    --repo kernelci/dashboard \
    --ref chore/deploy-sha-images \
    -f image_tag=<commit tag from the published image>
gh workflow run "Deploy Staging and E2E" \
  --repo kernelci/dashboard \
  --ref <branch-with-no-images>
  1. check-migrations succeeds.
  2. deploy-staging pulls that full SHA, retries for about 10 minutes, then fails with Timed out waiting for GHCR images.
  3. Containers are not recreated. The host still deletes and checks out dashboard-staging before the pull, so that directory changes.
  4. e2e-tests does not run.

3. Manual staging (live staging)

Same command as (2), --ref = the branch published in (1). This updates staging.

gh workflow run "Deploy Staging and E2E" \
  --repo kernelci/dashboard \
  --ref <branch>
  1. Run Summary: Deploying staging \` (``)`.
  2. The deploy log pulls ghcr.io/kernelci/dashboard/dashboard-backend:<full sha>, and the same for dashboard-frontend and dashboard-proxy.
  3. Do not use the staging site version string; that is git describe, not the image tag.

4. After merge to main (live staging; existing CI behavior)

Two runs, same merge commit. Actions at https://github.com/kernelci/dashboard/actions.
CI

  1. Left: CI for KernelCI Dashboard.
  2. Open the run with event push (not pull_request). Title is the merge commit.
  3. Run header shows the commit SHA (github.sha).
  4. Jobs: call-deploy-staging / Deploy Staging and E2E must run (skipped = you opened the PR CI run).
  5. Open job deploy-staging → run Summary:
    • Deploying staging \main` (``)` equals the header SHA.
    • Images pulled are …/dashboard-*:<same sha>, not :latest.
      Publish
  6. Left: Publish GHCR Images.
  7. Open the run for that same commit.
  8. Summary: Commit SHA: <sha> matches the SHA from the CI staging job.
    Discord only fires if staging fails; success is Actions Summary only.

Closes #2087.

@alanpeixinho
alanpeixinho marked this pull request as ready for review September 22, 2026 21:12
@alanpeixinho
alanpeixinho force-pushed the chore/deploy-sha-images branch from e9a1955 to bfb9c73 Compare September 23, 2026 12:58

@felipebergamin felipebergamin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two things that will bite us on the actual deploy path — left them inline.

Comment thread .github/workflows/ci.yaml Outdated
uses: ./.github/workflows/deploy-staging.yaml
secrets: inherit
with:
image_tag: ${{ github.sha }}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is going to race with Publish GHCR Images. Both still start on their own when something lands on main, and CI never waits for the images to actually be in the registry.

That used to be fine because staging built on the box. Now it pulls this SHA with --no-build, so if tests finish before all three images are pushed, deploy just fails. A retry/wait on pull would keep build and deploy as separate workflows and still make this reliable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — addressed in 7c53d27.

Staging deploy now retries docker compose pull (up to ~10 min) so it waits for the SHA tags in GHCR instead of racing Publish. Build and Deploy stay separate.

Comment thread .github/workflows/deploy-staging.yaml Outdated
cp ~/.env-staging dashboard-staging/.env &&
cd dashboard-staging &&
git checkout ${GITHUB_SHA} &&
git checkout ${IMAGE_TAG} &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This clone only has main, so git checkout will fail for a SHA that isn't on that branch. That's the hotfix path from #2087 — publish from a branch, then deploy that SHA. Actions checkout can fetch it; the server can't. Same thing in the production workflow.

Fetching the SHA first (git fetch origin "$IMAGE_TAG" then checkout) would cover that and also save us from cloning the whole history on every deploy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Also fixed in 7c53d27 (staging + production).

We now git fetch origin "$IMAGE_TAG" before checkout, and clone with --depth 1 so hotfix SHAs not on main still resolve without pulling full history.

outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

since we are here I think we can update the action version.

Suggested change
- uses: actions/checkout@v4
- uses: actions/checkout@v7

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in efdad26. Publish now uses actions/checkout@v7.

Comment on lines +26 to +41
resolve-ref:
if: github.repository == 'kernelci/dashboard' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
outputs:
sha: ${{ steps.git.outputs.sha }}
steps:
- name: Resolve commit SHA
id: git
env:
GH_TOKEN: ${{ github.token }}
REF: ${{ github.event.inputs.ref || github.sha }}
run: |
set -euo pipefail
sha=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${REF}" --jq .sha)
echo "sha=${sha}" >> "$GITHUB_OUTPUT"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do we need this? I'm not sure but I think we can pass the ref directly to the checkout action.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in efdad26. We check out inputs.ref (or github.sha on push) and take the image tag from git rev-parse HEAD, so a branch or tag still becomes one commit SHA. The job stays so a bad ref fails once before the three image builds.

Comment on lines 8 to 13
inputs:
ref:
description: Git ref to build (branch, tag, or commit SHA)
required: true
type: string

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe we can use the github selector instead of allowing an open input. The ref selected there will be in ${{ github.ref }} (e.g. ref/tags/release-tag) and ${{ github.ref_name }} (e.g. release-tag). The commit sha will be ${{ github.sha }}

image.png

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dont we loose the power to go to any commit with that? I believe it only brings tags and branches.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes, but I see this as a good thing to enforce having a branch or a tag to be deployed.

Otherwise we can select any commit and after some time would be hard to tell why that commit is on prod.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. The free-text ref is gone. A build comes from a branch or tag, and the image tag is the full github.sha.

steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.resolve-ref.outputs.sha }}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if I'm not mistaken we can use ${{ github.sha }}.
maybe we don't even need to provide this arg, it defaults to github.ref

https://github.com/actions/checkout#usage

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The image we want is inputs.ref, which can be a different SHA than the one from github.sha.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

not if we rely on "Use workflow from" selector as I said in my other comment

github.sha will carry the resolved sha for the selected ref, and for workflows dispatched by pushes it will carry the sha for the commit that triggered the workflow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Checkout follows the selected branch or tag, and the image tag is the full github.sha.

tags: |
${{ steps.meta.outputs.prefix }}/dashboard-frontend:latest
${{ steps.meta.outputs.prefix }}/dashboard-frontend:${{ github.sha }}
tags: ${{ steps.meta.outputs.prefix }}/dashboard-frontend:${{ needs.resolve-ref.outputs.sha }}${{ github.event_name == 'push' && format(',{0}/dashboard-frontend:latest', steps.meta.outputs.prefix) || '' }}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we can use a short sha to make easier to read the image tags.

- name: Get short SHA
  run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> "$GITHUB_ENV"

- name: Show short SHA
  run: echo "${{ env.SHORT_SHA }}"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I might be too much conservative (especially because we are a small repo), but I still prefer long hashes due to reduced collision chance.
Do you consider too problematic to keep long hash here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I just think that a short sha would be easier to read. I don't know about the chances of a collision. But consider this an optional.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Keeping the full SHA.

Comment on lines +6 to +9
image_tag:
description: GHCR image tag (Git commit SHA) to deploy
required: true
default: 'main'
type: string

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd replace this by github.ref_name

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

github.ref_name is the branch we are running the workflow from image_tag is the one we are using to build the image.

@felipebergamin felipebergamin Sep 28, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

My comments are considering a change to use the github ref selector.

If we select a tag there, github.ref_name will carry the tag name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. No open input. The selected branch or tag is deployed as its full github.sha.

- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.image_tag }}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ditto

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as above: checkout image_tag, not github.ref_name.

SSH_HOST: ${{ secrets.STAGING_HOST }}
SSH_KEY: ${{ secrets.STAGING_KEY }}
DASHBOARD_VERSION: ${{ github.event.inputs.tag }}
IMAGE_TAG: ${{ github.event.inputs.image_tag }}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

again, I'd use github.(ref | ref_name | sha) instead of relying in the input exactly as provided

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The host pulls image_tag. github.sha and github.ref_name are the workflow run, not that image.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

My comment consider that we would drop the open input and use the github ref selector.

If we select a tag there, github.ref_name will carry the tag name. It is basically the same that typing the tag name in the input.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. The open input is gone. Deploy uses the full github.sha of the selected branch or tag.

Comment thread .github/workflows/deploy-staging.yaml Outdated
Comment on lines +11 to +15
inputs:
image_tag:
description: GHCR image tag (Git commit SHA) to deploy
required: true
type: string

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as production. Staging can deploy a SHA other than the branch that started the workflow.

* Publish GHCR Images: optional ref on manual dispatch; tag images with resolved commit SHA; push `latest` only on push to main
* Staging: required `image_tag`; pull GHCR images with `--no-build`; manual `workflow_dispatch`; CI passes `github.sha` after tests
* Production: required `image_tag` (replaces unused `tag` default); export `IMAGE_TAG` on deploy; checkout deploy SHA on server
* Align staging `docker-compose.yml` image paths with `IMAGE_OWNER` / `IMAGE_REPOSITORY` from `.env.example`
* Update DEPLOYMENT.md and README for SHA-based deploy flow

Closes kernelci#2087
Retry staging pulls until SHA tags exist so CI does not race Publish.
Fetch the image_tag on the host before checkout so hotfix commits work.
Checkout the requested ref and tag images with git rev-parse HEAD.
Bump actions/checkout to v7.

Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi>
* Drop free-text ref and image_tag inputs; use the GitHub branch/tag selector
* Tag and deploy the full github.sha; keep latest only for pushes to main
* Remove resolve-ref; checkout already matches github.sha
* On the host, fetch the selected ref and check out that SHA
* Document the selector in DEPLOYMENT.md and README
Comment on lines 32 to 33
NEW_MIGRATIONS=$(git diff --name-only HEAD~1 HEAD -- '**/migrations/*.py' \
| grep -v '__init__.py' || true)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can we rely on this check?
if we dispatch this action in a branch, it will check only the last commit, migrations added in previous commits won't be notified.

I think HEAD~1 only works when we use squash and merge to main.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this would be tricky to implement in a more generic way, but we can include a proper comment to ensure that this requires the squash and merge.

Comment thread .github/workflows/deploy-staging.yaml Outdated
Comment on lines +85 to +86
git fetch --depth 1 origin "${GIT_REF}"
git checkout --detach "${IMAGE_TAG}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: There is a small chance that the ref could be updated between the start of the action and the point when the KCI server tries to fetch it. This could cause github.sha to differ from the actual SHA of the ref in the repository.

I don't think this is very likely to happen, but it's something we should be aware of.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. The host now fetches github.sha directly, so a push to the branch after the run starts cannot change the checkout.

git fetch --depth 1 origin "${IMAGE_TAG}"
git checkout --detach "${IMAGE_TAG}"

IMAGE_TAG is still the SHA of the branch or tag selected in "Use workflow from".

The Discord notice diffs HEAD~1, so it only covers the whole pull request while main is updated by one squash commit.

Assisted-by: Grok 4.7 <noreply@x.ai>
Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi>
Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi>
@tales-aparecida

Copy link
Copy Markdown
Contributor

I think I would be more comfortable with this PR if staging was in a different server.

@alanpeixinho

Copy link
Copy Markdown
Contributor Author

Yeah, I agree. Staging on its own server would be a good change. I believe #1516 already has some comments on this.

What we want from this PR is the rollback path from #2087. If a deploy goes bad, we redeploy an older commit SHA instead of being stuck on latest.

Do you see something we could still do in this PR to make the shared host less of a problem? Should we also hold this merge until #1516 is done? @nuclearcat might want to share his take on this too.

This branch has not been deployed

No deployments
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.

Improve Deployment Versioning and Ref Selection

3 participants