Merge pull request #1251 from vrtmrz/fix/cli-patch-image-tags

Fix CLI-only Docker patch image tags
This commit is contained in:
vorotamoroz
2026-10-14 21:43:49 +09:00
committed by GitHub
5 changed files with 126 additions and 3 deletions
+10 -1
View File
@@ -1,5 +1,5 @@
# Build and push the CLI Docker image to GitHub Container Registry (GHCR).
# Image tag format: <manifest-version>-cli
# Release image tag format: <release-version>-cli
# Example: 1.0.36-cli
#
# Stable releases are also tagged with their major-minor version and 'latest'.
@@ -72,6 +72,15 @@ jobs:
DRY_RUN: ${{ inputs.dry_run }}
run: |
VERSION=$(jq -r '.version' manifest.json)
if [[ "${REF}" == refs/tags/*-cli ]]; then
# CLI releases use their Git tag independently of the plug-in version.
VERSION="${REF#refs/tags/}"
VERSION="${VERSION%-cli}"
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$ ]]; then
printf 'Invalid CLI release tag: %s\n' "$REF" >&2
exit 1
fi
fi
MAJOR_MINOR=$(echo "${VERSION}" | cut -d. -f1,2)
SHORT_SHA=$(git rev-parse --short HEAD)
IMAGE="ghcr.io/${{ github.repository_owner }}/livesync-cli"
+58 -1
View File
@@ -311,11 +311,13 @@ Existing legacy Modules continue to register their handlers in `onBindFunction()
## Pre-release Policy
This policy governs plug-in pre-releases and their accompanying CLI images. For a correction needed only by the CLI, use [CLI-only patch releases](#cli-only-patch-releases).
- Use SemVer beta identifiers such as `1.0.0-beta.0` for immutable integration previews. Increment the beta number when a published preview needs a correction. Reserve `1.0.0-rc.0` for the first feature- and contract-frozen release candidate. Historical `-patchedN` releases remain unchanged in the release history.
- Publish a pre-release from an immutable reviewed tag, mark its GitHub Release as a pre-release, and do not replace the latest stable release.
- A plug-in review release may omit the CLI image when the CLI artefact is not part of the required validation. When a pre-release CLI image is published, it receives immutable version and SHA-qualified tags only; it must not advance `latest` or a stable major-minor tag.
- Keep a hyphenated pre-release's release pull request in draft and unmerged after BRAT validation. Reconcile the published version's metadata into its base branch through a reviewed metadata-only commit, then close the release pull request only through a separate maintainer action.
- Stage a stable version for BRAT by publishing its exact `x.y.z` tag initially as a GitHub pre-release with `prerelease=true` and `publish_cli=false`. The stable manifest version would otherwise make the CLI workflow advance `latest` and the major-minor image tag before validation.
- Stage a stable version for BRAT by publishing its exact `x.y.z` tag initially as a GitHub pre-release with `prerelease=true` and `publish_cli=false`. A stable CLI release tag would otherwise make the CLI workflow advance `latest` and the major-minor image tag before validation.
- After a staged stable version passes BRAT validation, remove the GitHub pre-release designation and make the exact release the latest stable release first. Confirm that promotion, then merge its exact release commit into the reviewed base branch and integrate it through the reviewed branch chain into the repository's default branch. This order prevents Community Directory review from inspecting a stable manifest version while the matching GitHub Release remains a pre-release. Publish stable CLI tags through a separate maintainer gate.
- If validation fails, leave every published tag unchanged and prepare the next pre-release or patch version.
@@ -333,6 +335,7 @@ Existing legacy Modules continue to register their handlers in `onBindFunction()
This workflow is for maintainers. Contributors should update `## Unreleased` for user-facing feature or fix PRs, but do not need to run the release workflows.
The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `release` GitHub Environment. Configure Environment protection in the repository settings so tag creation and release publication require maintainer approval.
For a fix needed only by the CLI, use [CLI-only patch releases](#cli-only-patch-releases). The steps below describe plug-in releases.
- Run the `Prepare Release PR` workflow with the target version and selected base branch. It creates the release branch, updates versions, confirms that Commonlib is locked to an immutable package version, moves the `## Unreleased` notes to the target version, commits the release preparation, pushes the branch, and opens a draft release PR. The base branch may already select the target development version; the workflow still runs the version lifecycle so that release-only metadata such as `versions.json` is recorded in the release commit.
- Do not tag the release branch when the PR is first created. Polish the release PR first, especially `updates.md`.
@@ -377,6 +380,60 @@ The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `rel
11. After a stable version passes, remove the pre-release designation and make the exact release the latest stable release. Confirm that promotion, then mark the release PR ready, merge the exact release commit into the selected base branch, and integrate it through the reviewed branch chain into the repository's default branch. Confirm that the default branch contains the matching stable metadata, then publish stable CLI tags through a separate maintainer gate if selected.
12. If validation fails, leave the PR in draft and do not move the published tags. Reconcile the published version's `updates.md` section and `versions.json` entry into the base branch as metadata only, then prepare the next patch or pre-release version from the remaining `## Unreleased` entries.
### CLI-only patch releases
Use this route when a correction affects only CLI commands, CLI host behaviour, or the CLI Docker image. For a change which also affects the Obsidian plug-in, use the plug-in release workflow above.
A CLI-only patch publishes a Docker image from a reviewed commit using `x.y.z-patchN-cli`, for example `1.0.36-patch1-cli`. Keep `x.y.z` as the underlying plug-in version and increment `N` for each CLI-only correction. The image version comes from the CLI Git tag. Keep the plug-in version metadata unchanged: assigning this tag does not require a version bump, a `versions.json` entry, a plug-in tag, or a GitHub Release. Record the fix under `## Unreleased`; leave the plug-in release-note lifecycle for the next plug-in release.
1. Review and integrate the CLI fix, including its relevant CLI or Docker regression tests. Select a source branch whose reviewed commit contains both the fix and the CLI release workflow. Record that exact commit and confirm that the proposed patch tag is unused:
```bash
CLI_RELEASE_BRANCH=main
CLI_PATCH_TAG=1.0.36-patch1-cli
git fetch origin "$CLI_RELEASE_BRANCH" --tags
CLI_RELEASE_COMMIT=$(git rev-parse "origin/${CLI_RELEASE_BRANCH}^{commit}")
git show --no-patch --oneline "$CLI_RELEASE_COMMIT"
git ls-remote --tags origin "refs/tags/$CLI_PATCH_TAG"
```
The last command must succeed without returning a tag. If the tag already exists, choose the next patch number; keep published tags unchanged.
2. Run the Docker workflow without publication before pushing the tag:
```bash
gh workflow run cli-docker.yml --ref "$CLI_RELEASE_BRANCH" --field dry_run=true --field force=false
```
Wait for success and verify that the run's head SHA is exactly `$CLI_RELEASE_COMMIT`. Both native amd64 and arm64 Docker E2E suites must pass. If the branch has advanced, review and validate the intended commit again. A branch dry run uses `edge` on `main` or a development tag on another branch; it validates the image and publication preparation without reserving the patch tag.
3. After maintainer approval of the exact commit and tag, create and push only the CLI tag:
```bash
git tag "$CLI_PATCH_TAG" "$CLI_RELEASE_COMMIT"
git push origin "refs/tags/$CLI_PATCH_TAG"
```
Pushing this tag with maintainer credentials starts `Build and Push CLI Docker Image` and publishes after both architecture validations pass. This route uses `cli-docker.yml`; the plug-in release preparation and finalisation workflows are not needed.
4. If automation pushed the tag using `GITHUB_TOKEN`, explicitly dispatch publication against that existing tag. Such a push does not start another workflow ([GitHub workflow trigger rules](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow#triggering-a-workflow-from-a-workflow)):
```bash
gh workflow run cli-docker.yml --ref "$CLI_PATCH_TAG" --field dry_run=false --field force=false
```
Use this dispatch when the tag push did not start publication, rather than starting a second publication alongside an existing run.
5. Confirm that the workflow completed and the published image contains `linux/amd64` and `linux/arm64`:
```bash
CLI_IMAGE="ghcr.io/vrtmrz/livesync-cli:$CLI_PATCH_TAG"
docker buildx imagetools inspect "$CLI_IMAGE"
docker run --rm "$CLI_IMAGE" --help
```
Retest the corrected CLI behaviour using this exact image tag. A patch receives its full version tag and a SHA-qualified tag, such as `1.0.36-patch1-sha-<commit>-cli`. Publishing that patch tag does not update `latest`, `edge`, or the stable major-minor tag. Consumers must select the full patch tag explicitly.
## Contribution Guidelines
- Follow existing code style and conventions
+2
View File
@@ -193,6 +193,8 @@ The generated systemd unit uses the wrapper's absolute path and does not depend
### Docker
CLI-only Docker patches use their full release tag, such as `ghcr.io/vrtmrz/livesync-cli:1.0.36-patch1-cli`. Patch and RC images receive version and SHA-qualified tags. The `latest` and major-minor tags are updated by ordinary stable releases. CLI image versions come from the release Git tag, so a CLI-only patch does not require changing the Obsidian plug-in version.
A Docker image is provided for headless / server deployments. Build from the repository root:
```bash
+7
View File
@@ -12,6 +12,13 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
## Unreleased
### CLI
#### Fixed
- We can select CLI-only Docker patches by their full release tag, such as `1.0.36-patch1-cli`, while keeping the Obsidian plug-in version.
- Patch and RC images receive version and SHA-qualified tags. The `latest` and major-minor tags remain reserved for ordinary stable releases.
## 1.0.36
13th October, 2026
+49 -1
View File
@@ -273,9 +273,51 @@ describe("CLI CI selection", () => {
true,
["1.0.36-rc.1-cli", "1.0.36-rc.1-sha-deadbee-cli"],
],
[
"1.0.36",
"refs/tags/1.0.36-patch1-cli",
"push",
"false",
true,
["1.0.36-patch1-cli", "1.0.36-patch1-sha-deadbee-cli"],
],
[
"1.0.36",
"refs/tags/1.0.36-rc.1-cli",
"push",
"false",
true,
["1.0.36-rc.1-cli", "1.0.36-rc.1-sha-deadbee-cli"],
],
[
"1.0.36",
"refs/tags/1.0.37-cli",
"push",
"false",
true,
["1.0.37-cli", "1.0-cli", "latest", "1.0.37-sha-deadbee-cli"],
],
[
"1.0.36",
"refs/tags/1.0.36-patch2-cli",
"workflow_dispatch",
"true",
false,
["1.0.36-patch2-cli", "1.0.36-patch2-sha-deadbee-cli"],
],
[
"1.0.36",
"refs/tags/1.0.36",
"workflow_dispatch",
"true",
false,
["1.0.36-cli", "1.0-cli", "latest", "1.0.36-sha-deadbee-cli"],
],
["1.0.36", "refs/tags/not-a-version-cli", "workflow_dispatch", "true", false, null],
["1.0.36", "refs/tags/1.0.36+build-cli", "push", "false", false, null],
["1.0.36", "refs/heads/main", "push", "false", true, ["edge"]],
["1.0.36", "refs/heads/test", "workflow_dispatch", "true", false, ["1.0.36-dev-sha-deadbee-cli"]],
] as const)("preserves tag selection for %s on %s", (version, ref, event, dryRun, push, tags) => {
] as const)("derives publication metadata for manifest %s on %s", (version, ref, event, dryRun, push, tags) => {
const metadata = workflow("cli-docker.yml").jobs.metadata.steps?.find((step) => step.id === "meta");
expect(metadata?.run).toBeDefined();
const directory = mkdtempSync(join(tmpdir(), "livesync-cli-image-metadata-"));
@@ -298,6 +340,12 @@ describe("CLI CI selection", () => {
},
encoding: "utf8",
});
if (tags === null) {
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("Invalid CLI release tag");
expect(existsSync(output)).toBe(false);
return;
}
expect(result.status, result.stderr).toBe(0);
const image = "ghcr.io/test-owner/livesync-cli";
expect(readFileSync(output, "utf8").trim().split("\n")).toEqual([