|
1 | | -name: Release preparation |
| 1 | +name: Release |
2 | 2 |
|
3 | 3 | on: |
4 | 4 | push: |
|
8 | 8 | permissions: |
9 | 9 | contents: read |
10 | 10 |
|
| 11 | +defaults: |
| 12 | + run: |
| 13 | + shell: bash |
| 14 | + |
11 | 15 | concurrency: |
12 | 16 | group: release-${{ github.ref }} |
13 | 17 | cancel-in-progress: false |
|
31 | 35 | run: | |
32 | 36 | test -s LICENSE |
33 | 37 |
|
34 | | - if [[ ! "$GITHUB_REF_NAME" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then |
35 | | - echo "Release tags must use stable semantic version format vMAJOR.MINOR.PATCH." >&2 |
| 38 | + if [[ ! "$GITHUB_REF_NAME" =~ ^v([1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then |
| 39 | + echo "Release tags must use stable semantic version format vMAJOR.MINOR.PATCH with a major version of at least 1." >&2 |
36 | 40 | exit 1 |
37 | 41 | fi |
38 | 42 |
|
@@ -80,3 +84,186 @@ jobs: |
80 | 84 | retention-days: 7 |
81 | 85 | compression-level: 0 |
82 | 86 | overwrite: true |
| 87 | + |
| 88 | + publish: |
| 89 | + name: Sign and publish release |
| 90 | + needs: prepare |
| 91 | + runs-on: ubuntu-latest |
| 92 | + timeout-minutes: 10 |
| 93 | + permissions: |
| 94 | + actions: read |
| 95 | + contents: write |
| 96 | + deployments: read |
| 97 | + environment: |
| 98 | + name: release |
| 99 | + |
| 100 | + steps: |
| 101 | + - name: Check release protections |
| 102 | + env: |
| 103 | + GH_TOKEN: ${{ github.token }} |
| 104 | + RELEASE_RULESET_ID: ${{ vars.RELEASE_RULESET_ID }} |
| 105 | + run: | |
| 106 | + : "${RELEASE_RULESET_ID:?Set RELEASE_RULESET_ID on the release environment before releasing.}" |
| 107 | +
|
| 108 | + environment="$(gh api "repos/${GITHUB_REPOSITORY}/environments/release")" |
| 109 | + if ! jq -e ' |
| 110 | + [.protection_rules[]? | select(.type == "required_reviewers")] as $rules | |
| 111 | + ($rules | length) == 1 and |
| 112 | + $rules[0].prevent_self_review == true and |
| 113 | + ($rules[0].reviewers | length) > 0 and |
| 114 | + .deployment_branch_policy == { |
| 115 | + protected_branches: false, |
| 116 | + custom_branch_policies: true |
| 117 | + } |
| 118 | + ' <<<"$environment" >/dev/null; then |
| 119 | + echo "The release environment must require a reviewer, prevent self-review, and use a custom tag policy." >&2 |
| 120 | + exit 1 |
| 121 | + fi |
| 122 | +
|
| 123 | + deployment_policies="$( |
| 124 | + gh api --paginate --slurp \ |
| 125 | + "repos/${GITHUB_REPOSITORY}/environments/release/deployment-branch-policies?per_page=100" |
| 126 | + )" |
| 127 | + if ! jq -e ' |
| 128 | + [.[] | .branch_policies[] | {name, type}] == [{name: "v*", type: "tag"}] |
| 129 | + ' <<<"$deployment_policies" >/dev/null; then |
| 130 | + echo "The release environment must allow only tags matching v*." >&2 |
| 131 | + exit 1 |
| 132 | + fi |
| 133 | +
|
| 134 | + ruleset="$(gh api "repos/${GITHUB_REPOSITORY}/rulesets/${RELEASE_RULESET_ID}")" |
| 135 | + if ! jq -e ' |
| 136 | + .target == "tag" and |
| 137 | + .enforcement == "active" and |
| 138 | + (.conditions.ref_name.include | sort) == ["refs/tags/v*"] and |
| 139 | + .conditions.ref_name.exclude == [] and |
| 140 | + ([.rules[].type] | index("creation") != null) and |
| 141 | + ([.rules[].type] | index("update") != null) and |
| 142 | + ([.rules[].type] | index("deletion") != null) |
| 143 | + ' <<<"$ruleset" >/dev/null; then |
| 144 | + echo "The release ruleset must actively restrict v* tag creation, update, and deletion." >&2 |
| 145 | + exit 1 |
| 146 | + fi |
| 147 | +
|
| 148 | + - name: Download release artifacts |
| 149 | + env: |
| 150 | + GH_TOKEN: ${{ github.token }} |
| 151 | + run: | |
| 152 | + gh run download "$GITHUB_RUN_ID" \ |
| 153 | + --repo "$GITHUB_REPOSITORY" \ |
| 154 | + --name "release-${GITHUB_REF_NAME}-${GITHUB_RUN_ID}" \ |
| 155 | + --dir dist |
| 156 | +
|
| 157 | + - name: Check publication preconditions |
| 158 | + env: |
| 159 | + GH_TOKEN: ${{ github.token }} |
| 160 | + run: | |
| 161 | + tag_commit="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_REF_NAME}" --jq .sha)" |
| 162 | + if [ "$tag_commit" != "$GITHUB_SHA" ]; then |
| 163 | + echo "The release tag no longer points to the workflow commit." >&2 |
| 164 | + exit 1 |
| 165 | + fi |
| 166 | +
|
| 167 | + release="$( |
| 168 | + gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/releases?per_page=100" | |
| 169 | + jq -c --arg tag "$GITHUB_REF_NAME" '[.[][] | select(.tag_name == $tag)][0] // empty' |
| 170 | + )" |
| 171 | + if [ -n "$release" ]; then |
| 172 | + if [ "$(jq -r .draft <<<"$release")" = "true" ]; then |
| 173 | + echo "An existing draft release must be inspected and removed before retrying ${GITHUB_REF_NAME}." >&2 |
| 174 | + else |
| 175 | + echo "A published release already exists for ${GITHUB_REF_NAME}." >&2 |
| 176 | + fi |
| 177 | + exit 1 |
| 178 | + fi |
| 179 | +
|
| 180 | + version="${GITHUB_REF_NAME#v}" |
| 181 | + checksum="dist/terraform-provider-kernel_${version}_SHA256SUMS" |
| 182 | + test -s "$checksum" |
| 183 | + (cd dist && sha256sum --check "$(basename "$checksum")") |
| 184 | +
|
| 185 | + - name: Check signing configuration |
| 186 | + env: |
| 187 | + EXPECTED_GPG_FINGERPRINT: ${{ vars.GPG_FINGERPRINT }} |
| 188 | + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} |
| 189 | + PASSPHRASE: ${{ secrets.PASSPHRASE }} |
| 190 | + run: | |
| 191 | + : "${EXPECTED_GPG_FINGERPRINT:?Set GPG_FINGERPRINT on the release environment before releasing.}" |
| 192 | + : "${GPG_PRIVATE_KEY:?Set GPG_PRIVATE_KEY on the release environment before releasing.}" |
| 193 | + : "${PASSPHRASE:?Set PASSPHRASE on the release environment before releasing.}" |
| 194 | +
|
| 195 | + - name: Import GPG key |
| 196 | + env: |
| 197 | + EXPECTED_GPG_FINGERPRINT: ${{ vars.GPG_FINGERPRINT }} |
| 198 | + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} |
| 199 | + run: | |
| 200 | + expected="$(printf '%s' "$EXPECTED_GPG_FINGERPRINT" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]')" |
| 201 | + key_details="$( |
| 202 | + printf '%s' "$GPG_PRIVATE_KEY" | |
| 203 | + gpg --batch --with-colons --import-options show-only --import 2>/dev/null |
| 204 | + )" |
| 205 | + mapfile -t fingerprints < <( |
| 206 | + awk -F: ' |
| 207 | + $1 == "sec" { primary = 1; next } |
| 208 | + primary && $1 == "fpr" { print $10; primary = 0 } |
| 209 | + ' <<<"$key_details" |
| 210 | + ) |
| 211 | + if [ "${#fingerprints[@]}" -ne 1 ] || [ "${fingerprints[0]}" != "$expected" ]; then |
| 212 | + echo "The private key must contain exactly the Registry signing key." >&2 |
| 213 | + exit 1 |
| 214 | + fi |
| 215 | +
|
| 216 | + printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import |
| 217 | +
|
| 218 | + - name: Sign checksums |
| 219 | + env: |
| 220 | + EXPECTED_GPG_FINGERPRINT: ${{ vars.GPG_FINGERPRINT }} |
| 221 | + PASSPHRASE: ${{ secrets.PASSPHRASE }} |
| 222 | + run: | |
| 223 | + expected="$(printf '%s' "$EXPECTED_GPG_FINGERPRINT" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]')" |
| 224 | + version="${GITHUB_REF_NAME#v}" |
| 225 | + checksum="dist/terraform-provider-kernel_${version}_SHA256SUMS" |
| 226 | + printf '%s' "$PASSPHRASE" | |
| 227 | + gpg --batch --pinentry-mode loopback --passphrase-fd 0 \ |
| 228 | + --local-user "$expected" --output "${checksum}.sig" --detach-sign "$checksum" |
| 229 | + gpg --verify "${checksum}.sig" "$checksum" |
| 230 | +
|
| 231 | + - name: Create draft release |
| 232 | + env: |
| 233 | + GH_TOKEN: ${{ github.token }} |
| 234 | + run: | |
| 235 | + version="${GITHUB_REF_NAME#v}" |
| 236 | + first_release_url="https://github.com/${GITHUB_REPOSITORY}/blob/${GITHUB_SHA}/docs/first-release.md" |
| 237 | + manifest="dist/terraform-provider-kernel_${version}_manifest.json" |
| 238 | + checksum="dist/terraform-provider-kernel_${version}_SHA256SUMS" |
| 239 | + signature="${checksum}.sig" |
| 240 | +
|
| 241 | + tag_commit="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_REF_NAME}" --jq .sha)" |
| 242 | + if [ "$tag_commit" != "$GITHUB_SHA" ]; then |
| 243 | + echo "The release tag no longer points to the workflow commit." >&2 |
| 244 | + exit 1 |
| 245 | + fi |
| 246 | +
|
| 247 | + gh release create "$GITHUB_REF_NAME" \ |
| 248 | + dist/terraform-provider-kernel_"${version}"_*.zip \ |
| 249 | + "$manifest" \ |
| 250 | + "$checksum" \ |
| 251 | + "$signature" \ |
| 252 | + --repo "$GITHUB_REPOSITORY" \ |
| 253 | + --verify-tag \ |
| 254 | + --draft \ |
| 255 | + --generate-notes \ |
| 256 | + --notes "First-release guidance: [supported surface and imports](${first_release_url})." \ |
| 257 | + --title "$GITHUB_REF_NAME" |
| 258 | +
|
| 259 | + - name: Publish release |
| 260 | + env: |
| 261 | + GH_TOKEN: ${{ github.token }} |
| 262 | + run: | |
| 263 | + tag_commit="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_REF_NAME}" --jq .sha)" |
| 264 | + if [ "$tag_commit" != "$GITHUB_SHA" ]; then |
| 265 | + echo "The release tag no longer points to the workflow commit." >&2 |
| 266 | + exit 1 |
| 267 | + fi |
| 268 | +
|
| 269 | + gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft=false |
0 commit comments