Skip to content

ci: release versioned images and notes - #80

Merged
uniqueg merged 11 commits into
devfrom
ci/62-release
Nov 14, 2026
Merged

uniqueg merged 11 commits into
devfrom
ci/62-release

Conversation

@uniqueg

@uniqueg uniqueg commented Nov 14, 2026 •

Copy link
Copy Markdown
Member

Summary

Automates versioned releases, following the org's release pattern and cloud-registry-ui's release workflow, deviating only where Krini needs it.

Stacked on #75 (it builds on #75's ci.yml): merge #75 first; this PR's diff shrinks to its own commits then.

  • .github/workflows/release.yml / Release, on v* tags (adapted from cloud-registry-ui):
    • prepare / Prepare release: tag format (vX.Y.Z, -alphaN/-betaN/-rcN), annotated tag with a signature GitHub verified, commit on main, versions in package.json and CITATION.cff equal to the tag, curated notes docs/releases/<version>.md in the org format (shared publish-release action, check-only), no published release of the tag yet, and a successful CI run for the push of the commit to dev.
    • push / Push release image (environment release): build once, push by digest with SBOM and provenance; stops if the version tag exists.
    • scan / Scan release image (no credentials): Trivy via the shared scan-image action, fails on fixable critical/high.
    • smoke / Smoke test (no credentials, Krini-specific): CI's smoke test on exactly the pushed digest (non-root, read-only root file system, login page, start page redirect, image optimizer).
    • release-image / Release image (environment release): attest build provenance, sign with Sigstore (keyless), tag X.Y.Z (and latest for the highest stable version on main).
    • publish / Publish release: GitHub release with the curated notes plus GitHub's generated notes under ## Commits and the **Full Changelog** line (shared publish-release action); pre-release until 1.0.0.
    • Deviation from cloud-registry-ui: the smoke job, and the VERSION build argument (Krini's Dockerfile sets the image's version label from it).
  • .github/workflows/scan.yml / Scan: weekly Trivy scan of the latest release image (as in cloud-registry-ui).
  • ci.yml: new readiness / Release readiness job (in the gate), so that release-prep PRs fail instead of the release: the versions in package.json and CITATION.cff must always agree; unless the version is tagged on an earlier commit, a date-released, release notes or a version bump after an earlier release start the release preparation, and then date-released must be set and the curated notes must exist, without TODOs, in the org format. prepare also requires date-released. (cloud-registry-ui runs this check in its docs job; Krini's docs job comes with docs: build the docs with MkDocs on Read the Docs #73, which can move it there if wanted.)
  • The CI smoke test moved into scripts/check-image.sh, which CI's image job and the release's smoke job both run.
  • Docs: docs/releasing.md (steps, pre-tag commands, repository settings, scheduled scan), docs/deployment.md (release tags X.Y.Z/latest, verifying release images), README (Versioning).

Closes #62

Settings the release environment needs

Checked by name only by the main session/uniqueg (agents can't read secrets):

  • Environment release: required reviewers (leads), deployments limited to v* tags (done per uniqueg).
  • Secrets in release (not repository secrets): DOCKERHUB_LOGIN, DOCKERHUB_TOKEN (allowed to push elixircloud/krini).
  • Optional repository variable DOCKERHUB_ORG (default elixircloud); forks must set it before a test tag.
  • The Docker Hub repository elixircloud/krini must be public (the scan and smoke jobs pull without login); it is already used by CI.
  • Rulesets: v* tags only by maintainers and immutable; immutable releases; main fast-forward only by maintainers (imported from cloud-registry-ui).
  • No new repository secrets; GITHUB_TOKEN only (contents: write in publish, actions: read in prepare).

Verification

  • actionlint 1.7.12 (with shellcheck 0.11.0) on ci.yml, release.yml, scan.yml: no findings; shellcheck scripts/check-image.sh: no findings.
  • The readiness script, extracted from ci.yml with yq, run locally in scratch repositories against the pinned publish.py checker: 13 scenarios behave as intended (missing CITATION.cff, missing version, version mismatch, undated before any release, dated without notes, notes with TODO, notes not in format, good notes, tagged on another commit, tagged on this commit, version bumped after a release without date, notes without date, other pre-release tagged).
  • The pre-tag commands of docs/releasing.md: bash -n passes. Running them against a commit was not possible in my sandbox.
  • scripts/check-image.sh: run by CI's Build image job on this PR (see checks).
  • Not verifiable without a tag: the release workflow end to end. Needs a fork dry run before the first real tag: in a fork, set DOCKERHUB_ORG to a test namespace first (the workflows fall back to elixircloud), create the release environment with test Docker Hub secrets, push a signed test tag (e.g. v0.0.1-rc1) on a commit that passed CI on the fork's dev and is on its main, with matching versions and notes, and check the target namespace in the job logs.
  • CI: all GitHub Actions checks pass, including Release readiness and Build image (which now runs scripts/check-image.sh). The external Vercel – krini check fails, as on other PRs and on dev (not required).

Review

Full lane (adversarial-reviewer and quality-reviewer, two rounds).

Sourcery (6 threads, all answered and resolved): fixed publish not needing readiness (a commit failing it could get dev tags) and serialized release-image across tags (stale latest decision); declined the Codecov token on same-repo PRs (as in cloud-registry-ui; write access needed), the existing-tag skip in readiness (tags are immutable, lightweight tags make it stricter), latest after a failed newer release (next patch moves it, as in cloud-registry-ui), and 0.x releases marked stable (the shared action's prerelease: auto already makes them pre-releases).

Declined review findings

  • Duplicated tag-lookup and digest checks in push, scan, smoke, release-image; the pre-tag commands in docs/releasing.md duplicating prepare; long header comment (medium/low, quality): kept as in cloud-registry-ui's release workflow so that the repos stay aligned and diffable; a shared script or action would be an org-wide change. The pin of publish-release now names the two other places to keep in sync.
  • Separate pull step in smoke (low): keeps "image can't be pulled" apart from smoke-test failures.
  • A stale date-released from the previous release isn't detected (low): the release-prep step in docs/releasing.md says to update it; a date comparison against tag dates would need more API calls than the gain is worth.
  • scan.yml picks the newest pre-release by creation date, not by version, before 1.0 (low): same as cloud-registry-ui; matters only for backport releases, which Krini doesn't make.

🤖 Generated with Claude Code

Summary by Sourcery

Automate validated, signed versioned releases for Krini images and GitHub releases, with vulnerability scanning, smoke tests, release-readiness checks, and supporting documentation.

New Features:

  • Add an automated signed-tag release workflow that validates release metadata, builds and scans versioned container images, smoke-tests the exact release digest, signs and attests images, publishes versioned Docker tags, and creates GitHub releases.
  • Add a scheduled workflow for recurring vulnerability scans of the latest published release image.

Enhancements:

  • Centralize the container smoke test in a reusable script shared by CI and release validation.
  • Add CI release-readiness checks to keep package and citation versions aligned and require complete, organization-formatted release notes.
  • Document the release process, image tags and verification steps, and update deployment guidance for signed versioned images.

CI:

  • Extend CI gating with release-readiness validation and reuse the shared image smoke test.

Deployment:

  • Publish immutable versioned images and update the stable latest tag for the highest stable release, with provenance attestations and Sigstore signatures.

Documentation:

  • Add maintainer release procedures and repository settings documentation, and update README and deployment documentation for versioned releases and image verification.

uniqueg and others added 8 commits November 14, 2026 19:18
Dependabot targets dev; the deployment guide describes the branch
model (dev default, main frozen, releases from v* tags), the dev
image tag and its signature identity, and that krini:staging is no
longer updated. The README links point to dev and to ci.yml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run CI on dev only. Bring back the unit and end-to-end tests that
were lost when ci.yml replaced checks.yml, as in cloud-registry-ui:
a Test job (Vitest with the coverage threshold, report to Codecov)
and End-to-end tests per browser in the Playwright image. The type
check moves into Build. The gate and publish need the new jobs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sha-<commit> tags of images built before the rename verify with the
staging identity; releases from v* tags are planned (#62).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release workflow runs the same smoke test on the release image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add release.yml, adapted from cloud-registry-ui: prepare checks the
signed tag, the commit on main, the versions in package.json and
CITATION.cff, the curated notes and the CI run; the image is pushed
by digest, scanned, smoke-tested as in CI, then attested, signed and
tagged X.Y.Z (latest for the highest stable version), and the GitHub
release is created with the shared publish-release action
(pre-releases until 1.0). scan.yml scans the latest release weekly.
docs/releasing.md describes the steps and the settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once CITATION.cff has a version that isn't tagged, CI requires the
same version in package.json and the curated release notes in the
organization's format, so that release-prep pull requests fail
instead of the release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release readiness now always requires the same version in
package.json and CITATION.cff, and treats a release as in
preparation once CITATION.cff has a date-released. The smoke test
runs a container of its own instead of one named app. Note where the
pin of publish-release must be kept in sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@uniqueg uniqueg added the type: ci / cd Related to continuous integration / delivery label Nov 14, 2026
@uniqueg uniqueg self-assigned this Nov 14, 2026
@vercel

vercel Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
krini Ready Ready Preview Nov 14, 2026 9:26pm UTC

@sourcery-ai

sourcery-ai Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Implementation

Extract the image smoke test into a reusable script so CI and release jobs can check the same container behavior.

  • scripts/check-image.sh L1-61

Configuration

Add the tagged-release workflow that validates the release, builds and scans the image by digest, smoke-tests it, then attests, signs, tags, and publishes it.

  • .github/workflows/release.yml L1-463

Add release-readiness checks to CI and require them before publishing development images and before the CI gate passes.

  • .github/workflows/ci.yml L459, L555-619, L630

Replace CI's inline image smoke test with the shared script used by the release workflow.

  • .github/workflows/ci.yml L269-271

Add a scheduled workflow to scan the latest published release image for vulnerabilities.

  • .github/workflows/scan.yml L1-81

Documentation

Document release tags, image verification, and configuration for deploying versioned images.

  • docs/deployment.md L10-29, L98-107, L133-136 (+2 small edits)

Add the release procedure, pre-tag checks, repository settings, and scheduled-scan guidance.

  • docs/releasing.md L1-164

Explain the versioned release images and link to the release instructions from the README.

  • README.md L180-185

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (0656839) to head (3d62461).

Additional details and impacted files
@@            Coverage Diff            @@
##               dev       #80   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           12        12           
  Lines          273       273           
  Branches        77        77           
=========================================
  Hits           273       273           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Release readiness also starts on release notes or a version bump
after an earlier release, and then requires date-released, which
prepare in release.yml and the pre-tag commands now check too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@uniqueg
uniqueg marked this pull request as ready for review November 14, 2026 20:58
@uniqueg
uniqueg requested a review from AnonymousXC as a code owner November 14, 2026 20:58
@sourcery-ai

sourcery-ai Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

Linked issues

Checked at commit 3d62461

#62 ci: release versioned images and notes · 2 of 3 met · 1 to check by hand

# Requirement Status Notes
1 Add a release workflow that checks the tag, commit and metadata; pushes, scans, signs, attests and tags the image; and creates a GitHub release from curated and generated notes, with pre-releases until 1.0. ✅ Met The prepare, push, scan, release-image and publish jobs implement this flow; release-image also smoke-tests the pushed digest.
2 Run releases in a protected release environment for v* tags, with leads reviewing, and have a maintainer create it before the first tag. 👀 Check by hand (external system) Check the repository’s release environment settings: the workflow uses that environment, and the docs specify the required reviewer and v* tag restriction, but the actual configuration is outside the diff.
3 Document how to prepare and tag a release and where release notes go. ✅ Met docs/releasing.md describes the preparation and tagging steps and specifies docs/releases/<version>.md for curated notes.

@sourcery-ai sourcery-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.

Hey - I've found 6 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path=".github/workflows/ci.yml" line_range="184" />
<code_context>
+      - name: Submit Report to Codecov
+        uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
+        with:
+          token: ${{ secrets.CODECOV_TOKEN }}
+          files: ./coverage/lcov.info
+          disable_search: true
</code_context>
<issue_to_address>
**Pull requests can expose the Codecov token**

When a same-repository pull request is opened by someone with write access and its workflow is modified to exfiltrate the secret, a contributor with write access can submit a same-repository pull request that changes the workflow to send `CODECOV_TOKEN` to an external host; because the Codecov step receives the secret on pull-request runs, the token can be leaked and abused for Codecov uploads.

Pass `CODECOV_TOKEN` only on trusted push runs, or otherwise ensure pull-request workflows cannot access it.
</issue_to_address>

### Comment 2
<location path=".github/workflows/release.yml" line_range="380" />
<code_context>
+      # of an older release nor a concurrent release moves `latest`
+      # back: the highest stable release among the tags on `main`, so
+      # that stray tags are ignored
+      - name: Check whether this is the highest release
+        id: latest
+        env:
</code_context>
<issue_to_address>
**Older release replaces latest**

When stable release image jobs overlap and a lower release checks the tags before a higher release is visible, then reaches the tag step after the higher release updates `latest`, the older job uses its stale `latest=true` decision in `imagetools create` and overwrites the newer digest, so users pulling `latest` get an older image.

Serialize stable release image tagging and recheck which release should own `latest` immediately before updating it.

Also at `.github/workflows/release.yml:51-53`, `.github/workflows/release.yml:403`, `.github/workflows/release.yml:412`.
</issue_to_address>

### Comment 3
<location path=".github/workflows/ci.yml" line_range="459" />
<code_context>
-    needs: [lint, typecheck, build, audit, image, push, scan]
-    # Docker Hub secrets; the environment only allows the branches `staging`
-    # and `dev`
+    needs: [lint, test, build, audit, e2e, image, push, scan]
+    # Docker Hub secrets; the environment only allows the branch `dev`
     environment: dev-images
</code_context>
<issue_to_address>
**Failed readiness still publishes dev**

When a push has passing `publish` dependencies but its `readiness` job fails, `publish` runs without depending on `readiness`, then signs and tags the commit's image as `dev` even though the commit failed CI.

Add `readiness` to the `publish` job's dependencies.

Also at `.github/workflows/ci.yml:436`.
</issue_to_address>

### Comment 4
<location path=".github/workflows/ci.yml" line_range="592" />
<code_context>
+          fi
+          # The commit of the (annotated) tag, if any: later commits are
+          # skipped, but not the tagged commit itself
+          tagged="$(git ls-remote origin "refs/tags/v${version}^{}" | cut -f1)"
+          if [[ -n "${tagged}" && "${tagged}" != "${SHA}" ]]; then
+            echo "v${version} is tagged: no release in preparation"
</code_context>
<issue_to_address>
**Invalid existing tag skips checks**

When a version tag already exists but is lightweight, points to another commit, or fails release validation, while the current prep change lacks required release metadata, `readiness` treats the existing tag as a completed release and exits successfully without checking the release notes or `date-released`; lightweight tags also evade the peeled-ref lookup. Incomplete or unreleasable version prep therefore passes CI.

Recognize lightweight and annotated tags, and skip prep checks only when the existing tag is confirmed to represent a completed, valid release.

Also at `.github/workflows/ci.yml:593`, `.github/workflows/ci.yml:600`.
</issue_to_address>

### Comment 5
<location path=".github/workflows/release.yml" line_range="393-394" />
<code_context>
+            cd repo
+            git fetch --quiet --filter=blob:none --tags "${REPOSITORY_URL}" \
+              "+refs/heads/main:refs/remotes/origin/main"
+            highest="$(git tag -l 'v*' --merged origin/main \
+              | grep -E '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' \
+              | sort -V | tail -n 1 || true)"
+            if [[ "${highest}" == "${TAG}" ]]; then
</code_context>
<issue_to_address>
**Latest points to an unreleased image**

When a newer stable version tag is on `main` but its release image has not been published, for example because its scan failed, the selection counts every matching stable tag merged into `main`, without checking whether its image passed the release scan and was tagged. If a newer release fails before publishing its image, this run skips updating `latest`, leaving it on an older release—or unset after the first release—so users pulling `latest` do not get the highest available release.

Select the highest stable version whose image has been successfully published, rather than the highest stable tag on `main` alone.
</issue_to_address>

### Comment 6
<location path=".github/workflows/release.yml" line_range="453" />
<code_context>
+      # release of it exists. Keep the pin in sync with the checker of the
+      # `readiness` job of `ci.yml` and with `docs/releasing.md`
+      - name: Check release notes
+        uses: elixir-cloud-aai/.github/actions/publish-release@31f22fccc1f3c597ed7c1d1f75b2f85522155348 # v1.1.0
+        with:
+          notes: docs/releases/${{ steps.version.outputs.version }}.md
</code_context>
<issue_to_address>
**Zero-major releases are marked stable**

When a stable 0.x tag, such as `v0.1.0`, is published, `prepare` computes `prerelease=true` for stable 0.x tags, but `publish` passes only `notes` and `commit` to the shared action, so the GitHub release is created as stable before 1.0.0.

Pass the computed prerelease output to the publish action so 0.x releases are marked as pre-releases.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 6 findings to address first, and if the release checks or image-tagging logic are wrong, the workflow could publish and sign a flawed image under a version tag and create a release that downstream deployments or users rely on. Reverting the workflow would not undo an already published release or uses of that image.

Blocking findings: .github/workflows/ci.yml:184, .github/workflows/release.yml:380, .github/workflows/ci.yml:459, .github/workflows/ci.yml:592, .github/workflows/release.yml:394, and 1 more


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread .github/workflows/ci.yml Resolved
Comment thread .github/workflows/release.yml Resolved
Comment thread .github/workflows/ci.yml Resolved
Comment thread .github/workflows/ci.yml Resolved
Comment thread .github/workflows/release.yml Resolved
Comment thread .github/workflows/release.yml Resolved
Publish image needs Release readiness, so that a commit that failed
it gets no dev tags. Release image runs one at a time across tags,
so that latest can't be moved back by an older decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	.github/workflows/ci.yml
#	docs/deployment.md
@uniqueg
uniqueg merged commit 02620ce into dev Nov 14, 2026
32 checks passed
@uniqueg
uniqueg deleted the ci/62-release branch November 14, 2026 21:29

This branch had an error being deployed

1 failed deployment
Preview – krini — 3d62461e Deployed Nov 14, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: ci / cd Related to continuous integration / delivery

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: release versioned images and notes

1 participant