-
Notifications
You must be signed in to change notification settings - Fork 29
397 lines (355 loc) · 17 KB
/
release.yml
File metadata and controls
397 lines (355 loc) · 17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
name: Create Release
run-name: "Release - ${{ inputs.module }} ${{ inputs.dry-run && '(dry-run)' || '' }}"
# General usage
# ---
# Using all default settings will:
# - Create a draft release for the specified module at the HEAD of the branch.
# - The version will be determined from the public API changes between the HEAD of the branch and the last release.
# - If the API contains breaking changes, it will error if the module is >=1.0.0.
# - For more information on the versioning heuristic, see: https://pkg.go.dev/golang.org/x/exp/cmd/gorelease
# ---
# 1. Go to the https://github.com/smartcontractkit/<repo>/actions/workflows/release.yml
# 2. Click "Run workflow"
# 3. Choose module
# 4. Click "Run workflow"
# Initial release usage
# ---
# This release workflow is primarily designed around subsequent releases.
# For the initial release of a module, you must follow the below instructions.
# ---
# 1. Commit the module, and add the module path to the below "module" input.
# - Merge these changes to the default branch (ideally)
# 2. Run the workflow with:
# - module: the path to the new module
# - version-override: the desired version for the initial release (e.g. 0.1.0)
# - base-ref-override: set to the default branch (e.g. main), or the commit SHA that added the module.
# Retroactive security patch releases
# ---
# In edge cases, you may need to create a retroactive release for non-latest but still in-use versions.
# ---
# 1. Create a branch from the version tag that requires patching (if not latest)
# 2. Make changes + commit + push
# 3. Run this workflow:
# - For "Use workflow from" - select the release branch selected (rather than the default branch)
# - If the HEAD of the release branch is not the commit to be released, set head-ref-override to the desired release commit.
# - base-ref-override: set to the version tag that the branch was created from (e.g. module/v1.2.3)
# - version-override: set the desired version for the retroactive release (e.g. 1.2.4)
# - do not include the full tag here, the tag created will be: <tag-prefix><version-override>
on:
workflow_dispatch:
inputs:
module:
# This is the path to the go module to release, relative to the repository root.
# To add new modules, simply add them as options here.
# The workflow will validate that there is a go.mod at the specified path before proceeding.
description: |
MODULE (required) - the path to the module to release. "." refers to the root module.
required: true
type: choice
# Keep this up-to-date with the modules in the repository.
options:
- "."
- "keystore"
- "observability-lib"
- "pkg/values"
- "pkg/workflows/sdk/v2/pb"
- "pkg/chipingress"
- "pkg/monitoring"
tag-prefix-override:
# Tags for go modules have some formatting requirements.
# For root modules, the default tag prefix is "v".
# For submodules, the tag must be prefixed with the module's path relative to the repository root.
# - For example, if the module is in "path/to/module", the default tag prefix would be "path/to/module/v". The tag prefix must not end with a "/".
# For other tagging formats, you can override them here.
# - For example, if you want to use a prefix like `module@v` you can set that here.
description: |
TAG PREFIX (override) - prefix for the release tag. Root module default = 'v', submodule default = 'path/to/module/v'.
required: false
type: string
version-override:
# This forces a specific version to be released, regardless of the computed diff and versioning recommendations.
# This is intended for edge cases where the heuristic recommendations are incorrect, or blocking a major version release.
# Required if base-ref-override is set.
description: |
VERSION (override) - forces a specific version of the release. Format should be MAJOR.MINOR.PATCH (e.g. 1.2.3), without the tag prefix.
required: false
type: string
head-ref-override:
# This overrides/forces the tag to point to something other than the HEAD of the branch the workflow is running on.
# This is only needed if the commit to be released is not the HEAD of the branch.
description: "HEAD REF (override) - the ref/sha the new release/tag will reference. Defaults to the HEAD of the current branch."
required: false
# default: ${{ github.ref_name }} - not possible here
type: string
base-ref-override:
# Normally, the workflow determines the latest version/release by comparing the git tags matching the desired format (see tag-prefix-override).
# This overrides the latest release/version to be a specific ref. The computed API diff will be between this ref and the head ref (or release-ref-override if specified).
# If this is set then version-override is required, as overriding the base ref breaks the normal version recommendation heuristic.
description: "BASE REF (override) - The most recent release/version/ref to compare against. Defaults to the latest (semver) tag with the associated tag prefix. version-override is required if this is set."
required: false
type: string
dry-run:
description: "DRY RUN - Run everything, except for release creation."
required: true
type: boolean
default: false
permissions: {}
jobs:
process-inputs:
name: Process Inputs
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
module: ${{ inputs.module }}
tag-prefix: ${{ steps.tag-prefix.outputs.tag-prefix }}
head-ref: ${{ steps.ref.outputs.head-ref }}
dry-run: ${{ inputs.dry-run }}
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Validate module input and existence of go.mod
shell: bash
env:
MODULE: ${{ github.event.inputs.module }}
run: |
set -euo pipefail
if [ -z "${MODULE}" ]; then
echo "::error::'module' input is required"
exit 1
fi
if [ ! -f "${MODULE}/go.mod" ]; then
echo "::error::No go.mod found at '${MODULE}/go.mod' (module input was '${MODULE}')"
exit 1
fi
- name: Default head ref
id: ref
shell: bash
env:
HEAD_REF_OVERRIDE: ${{ github.event.inputs.head-ref-override }}
DEFAULT_HEAD_REF: ${{ github.ref_name }}
run: |
set -euo pipefail
REF="${DEFAULT_HEAD_REF:-}"
if [ -n "${HEAD_REF_OVERRIDE}" ]; then
echo "Overriding head ref with head-ref-override input: ${HEAD_REF_OVERRIDE}"
REF="${HEAD_REF_OVERRIDE}"
fi
echo "head-ref=${REF}" | tee -a "${GITHUB_OUTPUT}"
- name: Check base-ref-override and version-override inputs
shell: bash
env:
BASE_REF_OVERRIDE: ${{ github.event.inputs.base-ref-override }}
VERSION_OVERRIDE: ${{ github.event.inputs.version-override }}
run: |
if [ -n "${BASE_REF_OVERRIDE}" ] && [ -z "${VERSION_OVERRIDE}" ]; then
echo "::error::base-ref-override input is set, but version-override is not set and is required when using base-ref-override."
exit 1
fi
- name: Determine tag prefix (input or default)
id: tag-prefix
shell: bash
env:
MODULE: ${{ inputs.module }}
TAG_PREFIX_OVERRIDE: ${{ github.event.inputs.tag-prefix-override }}
run: |
set -euo pipefail
if [ -n "${TAG_PREFIX_OVERRIDE}" ]; then
TAG_PREFIX="${TAG_PREFIX_OVERRIDE}"
else
if [ "${MODULE}" = "." ]; then
TAG_PREFIX="v"
else
TAG_PREFIX="${MODULE}/v"
fi
fi
if [[ "${TAG_PREFIX}" == */ ]]; then
echo "::error::tag-prefix must not end with '/'. Got: '${TAG_PREFIX}'"
exit 1
fi
echo "tag-prefix=${TAG_PREFIX}" | tee -a "${GITHUB_OUTPUT}"
- name: Validate version-override format
shell: bash
env:
VERSION_OVERRIDE: ${{ github.event.inputs.version-override }}
run: |
if [ -n "${VERSION_OVERRIDE}" ] && ! [[ "${VERSION_OVERRIDE}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::version-override must be in format MAJOR.MINOR.PATCH (e.g. 1.2.3). Got: '${VERSION_OVERRIDE}'"
exit 1
fi
release:
name: Version and Draft Release ${{ inputs.dry-run && '(dry-run)' || '' }}
needs: [process-inputs, approval-gate]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Determine latest version for module
if: ${{ inputs.base-ref-override == '' }}
id: get-latest-tag
uses: smartcontractkit/.github/actions/get-latest-tag@get-latest-tag/0.1.0
with:
tag-prefix: ${{ needs.process-inputs.outputs.tag-prefix }}
- name: Fallbacks
# This step exists to ensure there are always outputs that can always be referenced, even if they are unused.
# - `get-latest-tag` step is skipped if `base-ref-override` is set, which also requires `version-override` to be set.
# - So the fallback values here are never used, however they ensure no runtime errors from referencing non-existent outputs..
id: fallbacks
env:
LATEST_VERSION: ${{ steps.get-latest-tag.outputs.latest-version || 'unknown' }}
NEW_VERSIONS_JSON: ${{ steps.get-latest-tag.outputs.new-versions-json || '{}' }}
BASE_REF: ${{ inputs.base-ref-override || steps.get-latest-tag.outputs.latest-tag }}
run: |
if [ -z "${BASE_REF}" ]; then
# This should not happen.
echo "::error::Failed to determine a base ref. Failing."
exit 1
fi
echo "latest-version=${LATEST_VERSION}" | tee -a "${GITHUB_OUTPUT}"
echo "base-ref=${BASE_REF}" | tee -a "${GITHUB_OUTPUT}"
PATCH_VERSION_TAG=$(echo "${NEW_VERSIONS_JSON}" | jq -r '.patch.tag // "unknown"')
MINOR_VERSION_TAG=$(echo "${NEW_VERSIONS_JSON}" | jq -r '.minor.tag // "unknown"')
MAJOR_VERSION=$(echo "${NEW_VERSIONS_JSON}" | jq -r '.major.version // "unknown"')
echo "patch-version-tag=${PATCH_VERSION_TAG}" | tee -a "${GITHUB_OUTPUT}"
echo "minor-version-tag=${MINOR_VERSION_TAG}" | tee -a "${GITHUB_OUTPUT}"
echo "major-version=${MAJOR_VERSION}" | tee -a "${GITHUB_OUTPUT}"
- name: Diff the module (${{ steps.fallbacks.outputs.base-ref }}...${{ needs.process-inputs.outputs.head-ref }})
id: apidiff-go
uses: smartcontractkit/.github/actions/apidiff-go@apidiff-go/v2
with:
module-directory: ${{ needs.process-inputs.outputs.module }}
base-ref-override: ${{ steps.fallbacks.outputs.base-ref }}
head-ref-override: ${{ needs.process-inputs.outputs.head-ref }}
enforce-compatible: "false"
- name: Determine version (heuristic enforcement)
id: determine-version
env:
VERSION_RECOMMENDATION: ${{ steps.apidiff-go.outputs.version-recommendation }}
LATEST_VERSION: ${{ steps.fallbacks.outputs.latest-version }}
VERSION_OVERRIDE: ${{ inputs.version-override }}
TAG_PREFIX: ${{ needs.process-inputs.outputs.tag-prefix }}
NEW_PATCH_VERSION_TAG: ${{ steps.fallbacks.outputs.patch-version-tag }}
NEW_MINOR_VERSION_TAG: ${{ steps.fallbacks.outputs.minor-version-tag }}
NEW_MAJOR_VERSION: ${{ steps.fallbacks.outputs.major-version }}
shell: bash
run: |
if [ -n "${VERSION_OVERRIDE}" ]; then
echo "version-override is set to '${VERSION_OVERRIDE}', overriding version recommendation."
echo "tag=${TAG_PREFIX}${VERSION_OVERRIDE}" | tee -a "${GITHUB_OUTPUT}"
exit 0
fi
if [ "${NEW_PATCH_VERSION_TAG}" = "unknown" ] || [ "${NEW_MINOR_VERSION_TAG}" = "unknown" ] || [ "${VERSION_RECOMMENDATION}" = "unknown" ]; then
# This should not happen. These fallback values should only exist if base-ref-override/version-override is set.
echo "::error::Failed to determine new version tags or version recommendation. Failing."
exit 1
fi
# Parse major from "X.Y.Z"
CURRENT_MAJOR="${LATEST_VERSION%%.*}"
if ! [[ "${CURRENT_MAJOR}" =~ ^[0-9]+$ ]]; then
echo "::error::Unable to parse major version from latest-version '${LATEST_VERSION}'"
exit 1
fi
echo "Latest version: ${LATEST_VERSION}"
echo "Current major version: ${CURRENT_MAJOR}"
echo "Version recommendation from API diff: ${VERSION_RECOMMENDATION}"
if [ "${VERSION_RECOMMENDATION}" = "major" ]; then
if [ "${CURRENT_MAJOR}" -ge 1 ]; then
# based on gorelease heuristic: https://pkg.go.dev/golang.org/x/exp/cmd/gorelease
echo "::error::Incompatible API differences. A major release is not allowed on the same module path for modules versioned >=1.0.0."
echo "::error::After 1.0.0, major releases require a new module path. Make necessary changes, and rerun with version-override as ${NEW_MAJOR_VERSION} to override this check."
exit 1
fi
echo "::warning::Module is <1.0.0 and version recommendation is major. Changing to minor bump (${NEW_MINOR_VERSION_TAG})."
echo "::warning::If you want a major version release, re-run with version-override set to ${NEW_MAJOR_VERSION} to override this check."
echo "tag=${NEW_MINOR_VERSION_TAG}" | tee -a "${GITHUB_OUTPUT}"
exit 0
fi
# Non-major recommendations follow normal rules
case "${VERSION_RECOMMENDATION}" in
patch) echo "tag=${NEW_PATCH_VERSION_TAG}" | tee -a "${GITHUB_OUTPUT}" ;;
minor) echo "tag=${NEW_MINOR_VERSION_TAG}" | tee -a "${GITHUB_OUTPUT}" ;;
*) echo "::error::Unexpected version recommendation: '${VERSION_RECOMMENDATION}'" ; exit 1 ;;
esac
- name: Create draft release (${{ steps.determine-version.outputs.tag }})
env:
GH_TOKEN: ${{ github.token }}
DRY_RUN: ${{ inputs.dry-run }}
GITHUB_REPOSITORY: ${{ github.repository }}
TAG: ${{ steps.determine-version.outputs.tag }}
REF: ${{ needs.process-inputs.outputs.head-ref }}
API_DIFF_SUMMARY: ${{ steps.apidiff-go.outputs.summary-path }}
run: |
set -euo pipefail
# Build args array once
release_args=(
"${TAG}"
--repo "${GITHUB_REPOSITORY}"
--target "${REF}"
--title "${TAG}"
--notes-file "${API_DIFF_SUMMARY}"
--draft
--prerelease=false
)
if [ "${DRY_RUN}" = "true" ]; then
echo "DRY RUN - skipping release creation"
echo "Would have run:"
echo -n "gh release create "
printf '%q ' "${release_args[@]}"
echo
exit 0
fi
gh release create "${release_args[@]}"
# Used to enforce approvals before kicking off the rest of the jobs.
approval-gate:
name: Approval Gate ${{ inputs.dry-run && '(dry-run bypass)' || '' }}
needs: [review-context, process-inputs]
runs-on: ubuntu-latest
# Only require approval gate if not a dry-run
environment:
name: ${{ !inputs.dry-run && 'approval-gate-foundations' || '' }}
deployment: false
steps:
- name: Exit successfully
run: exit 0
review-context:
name: Review Context
runs-on: ubuntu-latest
steps:
- name: Display workflow context for reviewers
shell: bash
env:
WORKFLOW_ACTOR: ${{ github.actor }}
WORKFLOW_REF: ${{ github.ref_name }}
INPUTS_JSON: ${{ toJson(inputs) }}
run: |
{
echo "## 📋 Workflow Run Context"
echo ""
echo "### 👤 Initiator"
echo "**Triggered by:** $WORKFLOW_ACTOR"
echo "**Ref:** $WORKFLOW_REF"
echo ""
echo ""
echo "### 📥 Workflow Inputs"
echo "| Input | Value |"
echo "|-------|-------|"
# Dynamically generate input rows from JSON using jq
# This is safe because jq properly handles escaping
# Using \u0060 for backtick character
echo "$INPUTS_JSON" | jq -r '
to_entries |
sort_by(.key) |
.[] |
"| **\(.key)** | \u0060\(if .value == null or .value == "" then "(empty)" else .value end)\u0060 |"
'
echo ""
echo "---"
echo ""
echo "✓ Review the information above before approving this workflow run."
} >> "$GITHUB_STEP_SUMMARY"