diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 0ff0626..643f0dc 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -68,7 +68,7 @@ on: type: string default: "" source_digest: - description: "Fully qualified digest ref of an already-published image to promote (e.g. sisqueslabs/beacon-api@sha256:...). Required when bump_mode=promote — see trunk-ci-cd.yml's image_digest output." + description: "Fully qualified digest ref of an already-published image to promote (e.g. sisqueslabs/beacon-api@sha256:...) — see trunk-ci-cd.yml's image_digest output. Optional for bump_mode=promote: leave empty to auto-resolve the current :edge tag (the latest build validated on main)." required: false type: string default: "" @@ -130,10 +130,6 @@ jobs: echo "::error::bump_mode=release-train requires a non-empty next_version input." exit 1 fi - if [ "${{ inputs.bump_mode }}" = "promote" ] && [ -z "${{ inputs.source_digest }}" ]; then - echo "::error::bump_mode=promote requires a non-empty source_digest input." - exit 1 - fi - name: Checkout uses: actions/checkout@v7.0.1 @@ -189,7 +185,7 @@ jobs: # Legacy mode: manual workflow_dispatch releases bump relative to the # version currently in package.json. - name: Bump version (legacy) - if: inputs.bump_mode != 'release-train' + if: inputs.bump_mode == 'legacy' id: bump_legacy run: | if [ "${{ inputs.release_type }}" = "stable" ]; then @@ -200,12 +196,40 @@ jobs: echo "version=${NEW_VERSION#v}" >> "$GITHUB_OUTPUT" echo "tag=${NEW_VERSION}" >> "$GITHUB_OUTPUT" + # Promote mode: there is only one channel (stable) and no manual + # "version" input to ask for — the bump is computed the same way + # release-train-detect does it for its `main` channel: scan + # conventional commits since the latest stable tag (feat: -> minor, + # a breaking-change marker -> major, anything else -> patch). Trunk- + # based main only ever moves forward via reviewed PRs, so this range + # is exactly "what shipped since the last release." + - name: Set version (promote) + if: inputs.bump_mode == 'promote' + id: bump_promote + run: | + LATEST_STABLE=$(git tag -l 'v[0-9]*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || true) + if [ -n "$LATEST_STABLE" ]; then RANGE="${LATEST_STABLE}..HEAD"; else RANGE="HEAD"; fi + LOG=$(git log "$RANGE" --pretty='%s%n%b' 2>/dev/null || true) + if echo "$LOG" | grep -qiE '^[a-z]+(\([^)]*\))?!:' || echo "$LOG" | grep -qE '^BREAKING[ -]CHANGE:'; then + BUMP="major" + elif echo "$LOG" | grep -qiE '^feat(\([^)]*\))?:'; then + BUMP="minor" + else + BUMP="patch" + fi + NEW_VERSION=$(npm version "$BUMP" --no-git-tag-version) + echo "version=${NEW_VERSION#v}" >> "$GITHUB_OUTPUT" + echo "tag=${NEW_VERSION}" >> "$GITHUB_OUTPUT" + - name: Export version outputs id: bump run: | if [ "${{ inputs.bump_mode }}" = "release-train" ]; then echo "version=${{ steps.bump_train.outputs.version }}" >> "$GITHUB_OUTPUT" echo "tag=${{ steps.bump_train.outputs.tag }}" >> "$GITHUB_OUTPUT" + elif [ "${{ inputs.bump_mode }}" = "promote" ]; then + echo "version=${{ steps.bump_promote.outputs.version }}" >> "$GITHUB_OUTPUT" + echo "tag=${{ steps.bump_promote.outputs.tag }}" >> "$GITHUB_OUTPUT" else echo "version=${{ steps.bump_legacy.outputs.version }}" >> "$GITHUB_OUTPUT" echo "tag=${{ steps.bump_legacy.outputs.tag }}" >> "$GITHUB_OUTPUT" @@ -359,6 +383,23 @@ jobs: username: ${{ github.actor }} password: ${{ github.token }} + # Only runs when the caller didn't pass an explicit source_digest — + # the common case is "release whatever's currently on :edge" (the + # latest build trunk-ci-cd.yml validated on main), so this is what + # makes bump_mode=promote a true one-click release with no required + # inputs. Passing source_digest explicitly still overrides this, e.g. + # to release an earlier commit while main has since moved on. + - name: "Resolve source digest (auto: latest :edge build)" + if: inputs.bump_mode == 'promote' && inputs.source_digest == '' + id: resolve_digest + run: | + DIGEST=$(docker buildx imagetools inspect "${{ inputs.image_name }}:edge" | awk '/^Digest:/{print $2; exit}') + if [ -z "$DIGEST" ]; then + echo "::error::Could not resolve the :edge digest for ${{ inputs.image_name }}. Has trunk-ci-cd.yml published a build on main yet?" + exit 1 + fi + echo "digest=${{ inputs.image_name }}@${DIGEST}" >> "$GITHUB_OUTPUT" + # Build first WITHOUT pushing: if the image cannot be built, nothing is # published anywhere (no git tag, no registry tag, no GitHub Release). # Skipped for bump_mode=promote — see "Promote existing image" below. @@ -433,7 +474,7 @@ jobs: if: inputs.bump_mode == 'promote' env: TAGS: ${{ steps.tags.outputs.list }} - SOURCE_DIGEST: ${{ inputs.source_digest }} + SOURCE_DIGEST: ${{ inputs.source_digest != '' && inputs.source_digest || steps.resolve_digest.outputs.digest }} run: | ARGS=() while IFS= read -r tag; do diff --git a/README.md b/README.md index c1511c3..b966053 100644 --- a/README.md +++ b/README.md @@ -435,8 +435,19 @@ gains a `promote` bump mode alongside `legacy`/`release-train`. It skips the build entirely and retags an already-published digest — the one that went through `dev` and `pre` above — onto the release tags with `docker buildx imagetools create`, so the exact bytes validated in `pre` are -what ships to `prod`. Everything else (version bump, git tag, changelog, -GitHub Release) works exactly like `legacy` mode. +what ships to `prod`. Changelog and GitHub Release generation work exactly +like `legacy` mode. + +**Zero required inputs — this is a one-click release.** Unlike `legacy` +mode, `promote` never asks for a version bump type: there is only one +channel (`stable`) once a repo is trunk-based, so the bump (patch/minor/ +major) is computed automatically from conventional commits since the +latest stable tag — the same logic `release-train-detect` already uses for +its `main` channel. `source_digest` is optional too: leave it empty and the +workflow resolves the current `:edge` tag (the latest build `trunk-ci-cd.yml` +validated on `main`) automatically. Pass `source_digest` explicitly only for +the exceptional case of releasing an earlier commit while `main` has since +moved on. ```yaml name: Release @@ -444,12 +455,9 @@ name: Release on: workflow_dispatch: inputs: - version: - type: choice - options: [patch, minor, major] source_digest: - description: "Digest to promote (from a trunk-ci-cd.yml run's image_digest output)" - required: true + description: "Optional: digest to promote (from a trunk-ci-cd.yml run's image_digest output). Leave empty to auto-promote the latest :edge build." + required: false type: string jobs: @@ -457,10 +465,11 @@ jobs: uses: sisques-labs/workflows/.github/workflows/docker-release.yml@main with: image_name: sisqueslabs/my-app - version: ${{ inputs.version }} release_type: stable bump_mode: promote source_digest: ${{ inputs.source_digest }} + run_lint: false # no rebuild happens under promote — the digest was + run_test: false # already lint/tested when trunk-ci-cd.yml built it secrets: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/openspec/changes/trunk-based-ci-cd/design.md b/openspec/changes/trunk-based-ci-cd/design.md index 49302bc..5946694 100644 --- a/openspec/changes/trunk-based-ci-cd/design.md +++ b/openspec/changes/trunk-based-ci-cd/design.md @@ -68,6 +68,17 @@ No file this change touches is edited in place for its existing behavior: `trunk `docker-release.yml`'s existing git-cliff + GitHub Release steps apply unchanged when `release_type: stable`. Because there is only one channel once a repo migrates, the alpha/beta/rc tag-ignoring logic in the changelog range computation becomes dead code for that repo's future releases but is not removed here (other repos still exercise it via `legacy`/`release-train` modes). +### D8 — `promote` mode has zero required inputs (discovered during the beacon-api pilot) + +The first pilot implementation still asked a human for a `version` bump type (patch/minor/major) and a required `source_digest`, mirroring `legacy` mode's manual dropdown. That was a design bug, not an intentional choice: `bump_mode: promote`'s "Bump version" step accidentally ran the same code path as `legacy` (its `if:` condition was `!= 'release-train'`, which is also true for `promote`). + +Fixed: `promote` now computes both automatically — + +- **Version bump**: scanned from conventional commits since the latest stable tag (identical algorithm to `release-train-detect`'s `main`-channel logic: a breaking-change marker → major, `feat:` → minor, anything else → patch). There is only one channel once a repo is trunk-based, so there is nothing for a human to choose between. +- **Source digest**: when not passed explicitly, resolved from the current `:edge` tag (the latest build `trunk-ci-cd.yml` validated on `main`) via `docker buildx imagetools inspect`. An explicit `source_digest` still overrides this, for releasing an earlier commit while `main` has since moved on. + +This makes cutting a release a genuine one-click action (`workflow_dispatch` with no required fields), matching the spirit of the whole migration: humans decide *when* to release, the pipeline decides *what* the release contains. + ## Risks / Trade-offs - **[Risk] Multi-arch digest promotion is unproven** → Validate `imagetools create` against a real multi-platform image before beacon-api's pilot relies on it for an actual prod release. diff --git a/openspec/changes/trunk-based-ci-cd/tasks.md b/openspec/changes/trunk-based-ci-cd/tasks.md index 9835d35..984c5df 100644 --- a/openspec/changes/trunk-based-ci-cd/tasks.md +++ b/openspec/changes/trunk-based-ci-cd/tasks.md @@ -19,6 +19,13 @@ ## 4. Verification -- [ ] 4.1 Dry-run `trunk-ci-cd.yml` against a disposable test repo/branch to confirm the `build-and-publish` → `deploy-dev` → `deploy-pre` ordering holds +- [ ] 4.1 Dry-run `trunk-ci-cd.yml` against a disposable test repo/branch to confirm the `build-and-publish` → `deploy-dev` → `deploy-pre` ordering holds. **In progress** — beacon-api PR #23 is the live test. - [ ] 4.2 Dry-run `docker-release.yml` with `bump_mode: promote` against a real multi-arch image and confirm the promoted tag pulls correctly on both architectures -- [ ] 4.3 Confirm no existing repo's `release-train.yml`-based pipeline changed behavior after this PR merges +- [x] 4.3 Confirm no existing repo's `release-train.yml`-based pipeline changed behavior after this PR merges — `release-train-detect.test.sh` still 36/36 after every change to `docker-release.yml`; `release-train.yml` itself untouched + +## 5. `promote` mode zero-input fix (D8) + +- [x] 5.1 Separate `bump_mode: promote`'s version step from `legacy`'s — `promote` no longer runs the manual `npm version ${{ inputs.version }}` path +- [x] 5.2 Compute the version bump for `promote` automatically from conventional commits since the latest stable tag (mirrors `release-train-detect`'s `main`-channel logic) +- [x] 5.3 Make `source_digest` optional for `promote`: auto-resolve the current `:edge` tag's digest via `docker buildx imagetools inspect` when not passed explicitly +- [ ] 5.4 Dry-run a real `promote` release with zero inputs and confirm the resolved version + digest are correct — needs a real repo with an `:edge` build published (part of 4.1/4.2's live test)