diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 22e58ecdf..76f23a24e 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -11,6 +11,15 @@ on: - '.release-please-manifest.json' - 'charts/openbao-operator/Chart.yaml' workflow_dispatch: + inputs: + tag_target: + description: "Commit to tag. Use branch-head only to retry a failed draft release after a release-branch fix." + required: false + default: release-pr-merge + type: choice + options: + - release-pr-merge + - branch-head permissions: contents: read @@ -97,6 +106,7 @@ jobs: env: REPO: ${{ github.repository }} BASE_BRANCH: ${{ github.ref_name }} + TAG_TARGET: ${{ github.event_name == 'workflow_dispatch' && inputs.tag_target || 'release-pr-merge' }} GH_READ_TOKEN: ${{ github.token }} GH_WRITE_TOKEN: ${{ steps.app-token.outputs.token }} OPENBAO_OPERATOR_RELEASE_TAG_GPG_PASSPHRASE: ${{ secrets.OPENBAO_OPERATOR_RELEASE_TAG_GPG_PASSPHRASE }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f90a4b085..8f62fbafc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -115,6 +115,7 @@ jobs: ref: ${{ github.ref }} source_date_epoch: ${{ needs.prepare.outputs.source_date_epoch }} cache-scope: ${{ needs.prepare.outputs.build_tag }} + sign_images: true secrets: inherit rebuild: diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index 9e6b2a47f..ef60613a5 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -21,6 +21,11 @@ on: required: false default: trusted type: string + sign_images: + description: "Sign pushed image digests with keyless cosign before returning build outputs" + required: false + default: false + type: boolean outputs: manager_digest: description: "Digest of the manager image" @@ -134,6 +139,20 @@ jobs: subject-digest: ${{ steps.build.outputs.digest }} push-to-registry: true + - name: Install cosign + if: ${{ inputs.sign_images }} + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + with: + cosign-release: v3.0.4 + + - name: Sign image (keyless) + if: ${{ inputs.sign_images }} + env: + IMAGE_REF: ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}@${{ steps.build.outputs.digest }} + run: | + set -euo pipefail + cosign sign --yes --new-bundle-format=true "${IMAGE_REF}" + collect-digests: name: Collect Image Digests runs-on: *runner diff --git a/hack/ci/create-release-tag-and-draft.sh b/hack/ci/create-release-tag-and-draft.sh index 835e2d336..5673f9d64 100755 --- a/hack/ci/create-release-tag-and-draft.sh +++ b/hack/ci/create-release-tag-and-draft.sh @@ -9,6 +9,7 @@ MANIFEST_FILE="${MANIFEST_FILE:-.release-please-manifest.json}" CHART_FILE="${CHART_FILE:-charts/openbao-operator/Chart.yaml}" RELEASE_NOTES_DIR="${RELEASE_NOTES_DIR:-release-notes}" DRY_RUN="${DRY_RUN:-0}" +TAG_TARGET="${TAG_TARGET:-release-pr-merge}" GH_READ_TOKEN="${GH_READ_TOKEN:-${GH_TOKEN:-}}" GH_WRITE_TOKEN="${GH_WRITE_TOKEN:-${GH_TOKEN:-}}" @@ -23,6 +24,14 @@ if [[ "${DRY_RUN}" != "1" && -z "${GH_WRITE_TOKEN}" ]]; then exit 1 fi +case "${TAG_TARGET}" in + release-pr-merge | branch-head) ;; + *) + echo "TAG_TARGET must be either 'release-pr-merge' or 'branch-head', got '${TAG_TARGET}'" >&2 + exit 1 + ;; +esac + require_file() { local path="$1" if [[ ! -f "${path}" ]]; then @@ -203,6 +212,29 @@ if [[ "${manifest_at_merge}" != "${version}" || "${chart_version_at_merge}" != " exit 1 fi +tag_oid="${merge_oid}" + +if [[ "${TAG_TARGET}" == "branch-head" ]]; then + tag_oid="$(git rev-parse HEAD)" + + if ! git merge-base --is-ancestor "${merge_oid}" "${tag_oid}"; then + echo "branch-head tag target ${tag_oid} does not descend from release PR merge commit ${merge_oid}" >&2 + exit 1 + fi + + manifest_at_target="$(git show "${tag_oid}:${MANIFEST_FILE}" | jq -er '."."')" + chart_version_at_target="$(git show "${tag_oid}:${CHART_FILE}" | chart_value /dev/stdin "version")" + chart_app_version_at_target="$(git show "${tag_oid}:${CHART_FILE}" | chart_value /dev/stdin "appVersion")" + + if [[ "${manifest_at_target}" != "${version}" || "${chart_version_at_target}" != "${version}" || "${chart_app_version_at_target}" != "${version}" ]]; then + echo "release files at branch-head tag target ${tag_oid} do not match ${version}" >&2 + echo " manifest@target: ${manifest_at_target}" >&2 + echo " chart@target: ${chart_version_at_target}" >&2 + echo " appVersion@target: ${chart_app_version_at_target}" >&2 + exit 1 + fi +fi + notes_file="$(mktemp)" generated_notes_file="$(mktemp)" trap 'rm -f "${notes_file}" "${generated_notes_file}"' EXIT @@ -231,23 +263,23 @@ fi if git rev-parse -q --verify "refs/tags/${version}" >/dev/null 2>&1; then local_tag_commit="$(git rev-list -n1 "${version}")" - if [[ "${local_tag_commit}" != "${merge_oid}" ]]; then - echo "local tag ${version} points at ${local_tag_commit}, expected ${merge_oid}" >&2 + if [[ "${local_tag_commit}" != "${tag_oid}" ]]; then + echo "local tag ${version} points at ${local_tag_commit}, expected ${tag_oid}" >&2 exit 1 fi elif git ls-remote --exit-code --tags origin "refs/tags/${version}" >/dev/null 2>&1; then git fetch --no-tags origin "refs/tags/${version}:refs/tags/${version}" >/dev/null 2>&1 remote_tag_commit="$(git rev-list -n1 "${version}")" - if [[ "${remote_tag_commit}" != "${merge_oid}" ]]; then - echo "remote tag ${version} points at ${remote_tag_commit}, expected ${merge_oid}" >&2 + if [[ "${remote_tag_commit}" != "${tag_oid}" ]]; then + echo "remote tag ${version} points at ${remote_tag_commit}, expected ${tag_oid}" >&2 exit 1 fi else if [[ "${DRY_RUN}" == "1" ]]; then - echo "[dry-run] would create signed annotated tag ${version} at ${merge_oid}" + echo "[dry-run] would create signed annotated tag ${version} at ${tag_oid} (${TAG_TARGET})" else require_git_tag_signing - git tag -s "${version}" "${merge_oid}" -m "Release ${version}" + git tag -s "${version}" "${tag_oid}" -m "Release ${version}" git push origin "refs/tags/${version}" fi fi diff --git a/internal/adapter/security/cluster_image_verification_test.go b/internal/adapter/security/cluster_image_verification_test.go index 2d1d28dd7..1e1769587 100644 --- a/internal/adapter/security/cluster_image_verification_test.go +++ b/internal/adapter/security/cluster_image_verification_test.go @@ -71,6 +71,7 @@ func assertOperatorSubjectRegExp(t *testing.T, expr string) { "https://github.com/dc-tec/openbao-operator/.github/workflows/publish-edge.yml@refs/heads/main", "https://github.com/dc-tec/openbao-operator/.github/workflows/publish-nightly.yml@refs/heads/main", "https://github.com/dc-tec/openbao-operator/.github/workflows/reusable-build.yml@refs/heads/main", + "https://github.com/dc-tec/openbao-operator/.github/workflows/reusable-build.yml@refs/tags/0.2.1", } for _, subject := range trusted { if !re.MatchString(subject) { diff --git a/internal/port/security/image_verification.go b/internal/port/security/image_verification.go index 4bf787dd8..52f55ee6b 100644 --- a/internal/port/security/image_verification.go +++ b/internal/port/security/image_verification.go @@ -16,7 +16,7 @@ const ( defaultGitHubOIDCIssuerRegExp = "^https://token\\.actions\\.githubusercontent\\.com$" openBaoReleaseSubjectRegExp = "^https://github\\.com/openbao/openbao/\\.github/workflows/release\\.yml@refs/tags/v?[0-9A-Za-z][0-9A-Za-z._+-]*$" - operatorSubjectRegExp = "^https://github\\.com/dc-tec/openbao-operator/\\.github/workflows/(release\\.yml@refs/tags/.+|publish-edge\\.yml@refs/heads/main|publish-nightly\\.yml@refs/heads/main|reusable-build\\.yml@refs/heads/main)$" + operatorSubjectRegExp = "^https://github\\.com/dc-tec/openbao-operator/\\.github/workflows/(release\\.yml@refs/tags/.+|publish-edge\\.yml@refs/heads/main|publish-nightly\\.yml@refs/heads/main|reusable-build\\.yml@(refs/heads/main|refs/tags/.+))$" operatorInitOfficialRepository = "ghcr.io/dc-tec/openbao-init" operatorBackupOfficialRepository = "ghcr.io/dc-tec/openbao-backup"