Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/actions/create-release-bundle/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ inputs:
description: Whether to run in dry-run mode
required: false
default: "false"
bundle-revision:
description: |
Appended to `version` to form the bundle version, so a rebuild of a release becomes a new
bundle revision rather than colliding with the bundle the first build created. The artifacts
keep the clean release version. Set to `auto` to use the run id and attempt.
required: false
default: ""
entrypoint-path:
description: Path to the create-release-bundle entrypoint script (relative to workspace)
required: false
Expand All @@ -30,10 +37,15 @@ inputs:
`jf release-bundle-annotate` after `jf release-bundle-create`. Relative paths are resolved from the job workspace.
required: false
default: ""
outputs:
bundle-version:
description: Bundle version that was created, including the revision when one was requested
value: ${{ steps.create.outputs.bundle-version }}
runs:
using: composite
steps:
- name: Create Release Bundle
id: create
shell: bash
run: |
entrypoint="${{ inputs.entrypoint-path }}"
Expand All @@ -51,10 +63,19 @@ runs:
if [[ -n "${{ inputs.bundle-metadata-path }}" ]]; then
meta_arg=(--bundle-metadata "${{ inputs.bundle-metadata-path }}")
fi
revision_arg=()
revision="${{ inputs.bundle-revision }}"
if [[ "$revision" == "auto" ]]; then
revision="${{ github.run_id }}-${{ github.run_attempt }}"
fi
if [[ -n "$revision" ]]; then
revision_arg=(--revision "$revision")
fi
"$entrypoint" \
--project "${{ inputs.jf-project }}" \
--build-names "${{ inputs.build-names }}" \
--bundle-name "${{ inputs.bundle-name }}" \
--version "${{ inputs.version }}" \
"${meta_arg[@]}" \
"${revision_arg[@]}" \
$dry_run_arg
62 changes: 14 additions & 48 deletions .github/actions/delete-release-bundle/guard-promoted-beyond.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,66 +3,32 @@
# Matches tf-artifactory cleanup_exclude_downstream_of_dev (TEST, STAGE, PREVIEW, INTERNAL, PROD).
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/../../workflows/lib/jfrog-lifecycle.sh"

bundle_name="${1:?bundle name required}"
bundle_version="${2:?bundle version required}"
jf_project="${3:?jf project required}"
guard_label="${4:-promotion guard}"

records_path="/lifecycle/api/v2/promotion/records/${bundle_name}?project=${jf_project}&filter_by=${bundle_version}&order_by=created_millis&order_asc=true"
echo "${guard_label}: checking promotion records for ${bundle_name}/${bundle_version}..."
records_json='{"promotions":[]}'
set +e
fetched_records=$(jf rt curl -X GET "$records_path" -H "Accept: application/json" 2>jfrog-error.log)
status=$?
set -e

if [ "$status" -ne 0 ]; then
error_text="$(<jfrog-error.log)"
if [[ $error_text == *"404"* || $error_text == *"not found"* || $fetched_records == *"404"* ]]; then
echo "No existing release bundle promotion records found."
exit 0
fi
echo "::error::Failed to fetch promotion records for ${bundle_name}/${bundle_version}."
echo "$error_text"
exit "$status"
fi

records_json="$fetched_records"
echo "$records_json" | jq '.'

if echo "$records_json" | jq -e '.errors? | length > 0' >/dev/null; then
if echo "$records_json" | jq -e '.errors[]? | (.status | tostring) == "404"' >/dev/null; then
echo "::warning::Promotion records endpoint returned 404; treating this as no existing promotion records."
exit 0
fi
echo "::error::Failed to fetch promotion records for ${bundle_name}/${bundle_version}."
if ! records=$(jfrog_promotion_records "$bundle_name" "$jf_project"); then
echo "::error::${guard_label}: could not verify promotions for ${bundle_name}/${bundle_version}. Refusing to delete."
exit 1
fi

higher_envs=$(echo "$records_json" | jq -r --arg version "$bundle_version" '
[(.promotions // [])[]?
| select((.release_bundle_version // .releaseBundleVersion // .version // "") == $version)
| select((.status // "COMPLETED") == "COMPLETED")
| (.environment // .target_environment // .targetEnvironment // empty)
| select(. == "TEST" or . == "STAGE" or . == "PREVIEW" or . == "INTERNAL" or . == "PROD")]
| unique
| join(", ")
')
if [ -n "$higher_envs" ]; then
echo "::error::Release bundle ${bundle_name}/${bundle_version} already promoted beyond DEV (${higher_envs}). Refusing to delete."
stages=$(jfrog_promotion_stages "$records" "$bundle_version")
beyond_dev=$(echo "$stages" | grep -Fx -e TEST -e STAGE -e PREVIEW -e INTERNAL -e PROD | paste -sd, - || true)

if [[ -n $beyond_dev ]]; then
echo "::error::Release bundle ${bundle_name}/${bundle_version} is promoted beyond DEV (${beyond_dev}). Refusing to delete. Remove the promotion first if this is intended."
exit 1
fi

existing_envs=$(echo "$records_json" | jq -r --arg version "$bundle_version" '
[(.promotions // [])[]?
| select((.release_bundle_version // .releaseBundleVersion // .version // "") == $version)
| select((.status // "COMPLETED") == "COMPLETED")
| (.environment // .target_environment // .targetEnvironment // empty)]
| unique
| join(", ")
')
if [ -n "$existing_envs" ]; then
echo "Existing release bundle promotions (${guard_label}): ${existing_envs}."
if [[ -n $stages ]]; then
echo "${guard_label}: existing promotions: $(echo "$stages" | paste -sd, -)."
else
echo "No existing release bundle promotions found (${guard_label})."
echo "${guard_label}: no existing promotions."
fi
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bats

setup() {
GUARD="${BATS_TEST_DIRNAME}/../../guard-promoted-beyond.sh"
export JF_RECORDS="${BATS_TEST_TMPDIR}/records.json"

mkdir -p "${BATS_TEST_TMPDIR}/bin"
cat >"${BATS_TEST_TMPDIR}/bin/jf" <<'STUB'
#!/usr/bin/env bash
if [[ $1 == "config" && $2 == "export" ]]; then
printf '%s' '{"url":"https://jfrog.test/","accessToken":"tkn"}' | base64 -w0
exit 0
fi
exit 0
STUB
cat >"${BATS_TEST_TMPDIR}/bin/curl" <<'STUB'
#!/usr/bin/env bash
cat >/dev/null
if [[ -n ${CURL_FAILS-} ]]; then
echo "curl: (22) HTTP 503" >&2
exit 22
fi
cat "$JF_RECORDS"
exit 0
STUB
chmod +x "${BATS_TEST_TMPDIR}/bin/jf" "${BATS_TEST_TMPDIR}/bin/curl"
export PATH="${BATS_TEST_TMPDIR}/bin:$PATH"
cd "$BATS_TEST_TMPDIR" || exit 1
}

# Each entry is "version:stage".
records() {
local entries=("$@") json="[]" e version stage
for e in "${entries[@]}"; do
version="${e%%:*}"
stage="${e##*:}"
json=$(echo "$json" | jq --arg v "$version" --arg s "$stage" \
'. + [{release_bundle_version: $v, environment: $s, status: "COMPLETED"}]')
done
echo "{\"promotions\": $json}" >"$JF_RECORDS"
}

guard() {
run bash "$GUARD" my-bundle "$1" test "test guard"
}

@test "refuses a version promoted beyond DEV" {
records "1.0.0:DEV" "1.0.0:TEST"
guard 1.0.0
[ "$status" -eq 1 ]
[[ $output == *"promoted beyond DEV"* ]]
[[ $output == *"TEST"* ]]
}

@test "refuses at every stage past DEV" {
for stage in TEST STAGE PREVIEW INTERNAL PROD; do
records "1.0.0:$stage"
guard 1.0.0
[ "$status" -eq 1 ]
[[ $output == *"promoted beyond DEV"* ]]
done
}

@test "allows a version promoted only to DEV" {
records "1.0.0:DEV"
guard 1.0.0
[ "$status" -eq 0 ]
[[ $output == *"existing promotions: DEV"* ]]
}

@test "allows a version with no promotions" {
records
guard 1.0.0
[ "$status" -eq 0 ]
[[ $output == *"no existing promotions"* ]]
}

@test "ignores promotions belonging to another version" {
records "9.9.9:PROD" "1.0.0:DEV"
guard 1.0.0
[ "$status" -eq 0 ]
}

@test "refuses when the records cannot be read" {
records "1.0.0:DEV"
CURL_FAILS=1 guard 1.0.0
[ "$status" -eq 1 ]
[[ $output == *"could not verify promotions"* ]]
}
50 changes: 35 additions & 15 deletions .github/workflows/create-release-bundle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,22 @@ This workflow creates JFrog release bundles by bundling one or more builds into

## Inputs

| Input | Description | Required | Default |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------- |
| `jf-project` | JFrog Artifactory project name | Yes | - |
| `jf-build-names` | Comma-separated list of `build-name:version` pairs to include (e.g. `"app-build:1.2.3,client-build:2.1.0"`) | Yes | - |
| `jf-bundle-name` | Name for the release bundle | Yes | - |
| `version` | Version of the release bundle | Yes | - |
| `jf-url` | JFrog Artifactory URL | No | `https://artifact.aerospike.io` |
| `oidc-provider-name` | OIDC provider name for authentication | No | `gh-aerospike` |
| `oidc-audience` | OIDC audience for authentication | No | `aerospike` |
| `runs-on` | The runner to use for the build | No | `ubuntu-22.04` |
| `gh-checkout-path` | Directory to checkout the shared-workflows repository into | No | `shared-workflows` |
| `gh-workflows-ref` | Git ref for shared-workflows (**should match `uses:`**) | Yes | - |
| `dry-run` | Whether to run in dry-run mode | No | `false` |
| `bundle-metadata-path` | Optional path to `.maven-bundle-metadata.json` (e.g. detect-artifacts `bundle-metadata-path`). Applied as bundle properties after create. | No | _(empty)_ |
| `gh-bundle-metadata-artifact-name` | When set, downloads this GitHub artifact and uses the contained `.maven-bundle-metadata.json` (e.g. `reusable_deploy-artifacts` output `bundle-metadata-artifact-name`). Overrides `bundle-metadata-path` when both are set. | No | _(empty)_ |
| Input | Description | Required | Default |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------- |
| `jf-project` | JFrog Artifactory project name | Yes | - |
| `jf-build-names` | Comma-separated list of `build-name:version` pairs to include (e.g. `"app-build:1.2.3,client-build:2.1.0"`) | Yes | - |
| `jf-bundle-name` | Name for the release bundle | Yes | - |
| `version` | Version of the release bundle | Yes | - |
| `bundle-revision` | Appended to `version` to form the bundle version, so a rebuild of a release becomes a new bundle revision instead of colliding with the existing bundle. Artifacts keep the clean release version. Use `auto` for the run id and attempt. | No | `""` |
| `jf-url` | JFrog Artifactory URL | No | `https://artifact.aerospike.io` |
| `oidc-provider-name` | OIDC provider name for authentication | No | `gh-aerospike` |
| `oidc-audience` | OIDC audience for authentication | No | `aerospike` |
| `runs-on` | The runner to use for the build | No | `ubuntu-22.04` |
| `gh-checkout-path` | Directory to checkout the shared-workflows repository into | No | `shared-workflows` |
| `gh-workflows-ref` | Git ref for shared-workflows (**should match `uses:`**) | Yes | - |
| `dry-run` | Whether to run in dry-run mode | No | `false` |
| `bundle-metadata-path` | Optional path to `.maven-bundle-metadata.json` (e.g. detect-artifacts `bundle-metadata-path`). Applied as bundle properties after create. | No | _(empty)_ |
| `gh-bundle-metadata-artifact-name` | When set, downloads this GitHub artifact and uses the contained `.maven-bundle-metadata.json` (e.g. `reusable_deploy-artifacts` output `bundle-metadata-artifact-name`). Overrides `bundle-metadata-path` when both are set. | No | _(empty)_ |

## Example Usage

Expand Down Expand Up @@ -76,3 +77,22 @@ Run the basic test suite:
```bash
.github/workflows/create-release-bundle/test-entrypoint.sh
```

## Bundle revisions

A release version alone is not unique per build, so rebuilding a release collides with the
bundle its first build created. Set `bundle-revision` to give each build its own bundle
version:

```yaml
with:
version: 1.2.3
bundle-revision: auto # bundle becomes 1.2.3-<run_id>-<run_attempt>
```

The artifacts inside keep the clean release version. Only the bundle version carries the
revision, which is what lets two builds of one release exist at once so a failed build can
be superseded. See [Promote Release Bundle](../promote-release-bundle/README.md).

The resolved bundle version is available as the `bundle-version` output, and is the value to
pass to the promotion workflow.
Loading