Skip to content

Commit f7597f9

Browse files
committed
Gate signed provider publication
Require protected release approval before importing the Registry key, signing the prepared checksum, and publishing retry-safe GitHub Release assets.
1 parent 6a40866 commit f7597f9

6 files changed

Lines changed: 362 additions & 20 deletions

File tree

.github/workflows/release.yml

Lines changed: 197 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
1-
name: Release preparation
1+
name: Release
22

33
on:
44
push:
55
tags:
66
- "v*"
7+
workflow_dispatch:
8+
inputs:
9+
version:
10+
description: Stable version to prepare without publishing
11+
required: true
12+
default: v0.0.1
13+
type: string
714

815
permissions:
916
contents: read
1017

18+
env:
19+
RELEASE_TAG: ${{ inputs.version || github.ref_name }}
20+
21+
defaults:
22+
run:
23+
shell: bash
24+
1125
concurrency:
1226
group: release-${{ github.ref }}
1327
cancel-in-progress: false
@@ -31,8 +45,13 @@ jobs:
3145
run: |
3246
test -s LICENSE
3347
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
48+
if [[ ! "$RELEASE_TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
49+
echo "Release versions must use stable semantic version format vMAJOR.MINOR.PATCH." >&2
50+
exit 1
51+
fi
52+
53+
if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ] && [ "$GITHUB_REF" != "refs/heads/main" ]; then
54+
echo "Manual release preflight must run from main." >&2
3655
exit 1
3756
fi
3857
@@ -47,6 +66,15 @@ jobs:
4766
exit 1
4867
fi
4968
69+
- name: Create local preflight tag
70+
if: github.event_name == 'workflow_dispatch'
71+
run: |
72+
if git show-ref --verify --quiet "refs/tags/${RELEASE_TAG}"; then
73+
echo "Release version ${RELEASE_TAG} already exists." >&2
74+
exit 1
75+
fi
76+
git tag "$RELEASE_TAG" "$GITHUB_SHA"
77+
5078
- name: Set up Go
5179
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
5280
with:
@@ -60,18 +88,18 @@ jobs:
6088
version: v2.17.0
6189
args: release --clean --skip=publish,sign
6290
env:
63-
GORELEASER_CURRENT_TAG: ${{ github.ref_name }}
91+
GORELEASER_CURRENT_TAG: ${{ env.RELEASE_TAG }}
6492

6593
- name: Verify release artifacts
6694
run: |
67-
version="${GITHUB_REF_NAME#v}"
95+
version="${RELEASE_TAG#v}"
6896
cp terraform-registry-manifest.json "dist/terraform-provider-kernel_${version}_manifest.json"
6997
bash scripts/check-release-artifacts.sh "$version"
7098
7199
- name: Upload release artifacts
72100
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
73101
with:
74-
name: release-${{ github.ref_name }}-${{ github.run_id }}
102+
name: release-${{ env.RELEASE_TAG }}-${{ github.run_id }}
75103
path: |
76104
dist/*.zip
77105
dist/*_manifest.json
@@ -80,3 +108,166 @@ jobs:
80108
retention-days: 7
81109
compression-level: 0
82110
overwrite: true
111+
112+
preflight:
113+
name: Verify protected release path
114+
if: github.event_name == 'workflow_dispatch'
115+
needs: prepare
116+
runs-on: ubuntu-latest
117+
timeout-minutes: 10
118+
permissions:
119+
actions: read
120+
contents: read
121+
deployments: read
122+
environment:
123+
name: release
124+
125+
steps:
126+
- name: Checkout
127+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
128+
with:
129+
persist-credentials: false
130+
131+
- name: Check release protections
132+
env:
133+
GH_TOKEN: ${{ github.token }}
134+
RELEASE_REVIEWERS: ${{ vars.RELEASE_REVIEWERS }}
135+
RELEASE_RULESET_ID: ${{ vars.RELEASE_RULESET_ID }}
136+
run: bash scripts/check-release-protections.sh
137+
138+
- name: Download release artifacts
139+
env:
140+
GH_TOKEN: ${{ github.token }}
141+
run: |
142+
gh run download "$GITHUB_RUN_ID" \
143+
--repo "$GITHUB_REPOSITORY" \
144+
--name "release-${RELEASE_TAG}-${GITHUB_RUN_ID}" \
145+
--dir dist
146+
147+
- name: Verify downloaded checksums
148+
run: |
149+
version="${RELEASE_TAG#v}"
150+
checksum="dist/terraform-provider-kernel_${version}_SHA256SUMS"
151+
test -s "$checksum"
152+
(cd dist && sha256sum --check "$(basename "$checksum")")
153+
154+
- name: Sign and verify checksums
155+
env:
156+
EXPECTED_GPG_FINGERPRINT: ${{ vars.GPG_FINGERPRINT }}
157+
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
158+
PASSPHRASE: ${{ secrets.PASSPHRASE }}
159+
run: bash scripts/sign-release-checksum.sh "${RELEASE_TAG#v}"
160+
161+
publish:
162+
name: Sign and publish release
163+
if: github.event_name == 'push'
164+
needs: prepare
165+
runs-on: ubuntu-latest
166+
timeout-minutes: 10
167+
permissions:
168+
actions: read
169+
contents: write
170+
deployments: read
171+
environment:
172+
name: release
173+
174+
steps:
175+
- name: Checkout
176+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
177+
with:
178+
persist-credentials: false
179+
180+
- name: Check release protections
181+
env:
182+
GH_TOKEN: ${{ github.token }}
183+
RELEASE_REVIEWERS: ${{ vars.RELEASE_REVIEWERS }}
184+
RELEASE_RULESET_ID: ${{ vars.RELEASE_RULESET_ID }}
185+
run: bash scripts/check-release-protections.sh
186+
187+
- name: Download release artifacts
188+
env:
189+
GH_TOKEN: ${{ github.token }}
190+
run: |
191+
gh run download "$GITHUB_RUN_ID" \
192+
--repo "$GITHUB_REPOSITORY" \
193+
--name "release-${GITHUB_REF_NAME}-${GITHUB_RUN_ID}" \
194+
--dir dist
195+
196+
- name: Check publication preconditions
197+
env:
198+
GH_TOKEN: ${{ github.token }}
199+
run: |
200+
tag_commit="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_REF_NAME}" --jq .sha)"
201+
if [ "$tag_commit" != "$GITHUB_SHA" ]; then
202+
echo "The release tag no longer points to the workflow commit." >&2
203+
exit 1
204+
fi
205+
206+
release="$(
207+
gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/releases?per_page=100" |
208+
jq -c --arg tag "$GITHUB_REF_NAME" '[.[][] | select(.tag_name == $tag)][0] // empty'
209+
)"
210+
if [ -n "$release" ]; then
211+
if [ "$(jq -r .draft <<<"$release")" = "true" ]; then
212+
echo "An existing draft release must be inspected and removed before retrying ${GITHUB_REF_NAME}." >&2
213+
else
214+
echo "A published release already exists for ${GITHUB_REF_NAME}." >&2
215+
fi
216+
exit 1
217+
fi
218+
219+
version="${GITHUB_REF_NAME#v}"
220+
checksum="dist/terraform-provider-kernel_${version}_SHA256SUMS"
221+
test -s "$checksum"
222+
(cd dist && sha256sum --check "$(basename "$checksum")")
223+
224+
- name: Sign and verify checksums
225+
env:
226+
EXPECTED_GPG_FINGERPRINT: ${{ vars.GPG_FINGERPRINT }}
227+
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
228+
PASSPHRASE: ${{ secrets.PASSPHRASE }}
229+
run: bash scripts/sign-release-checksum.sh "${GITHUB_REF_NAME#v}"
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+
notes=()
241+
if [ "$GITHUB_REF_NAME" = "v0.0.1" ]; then
242+
notes=(--notes "First-release guidance: [supported surface and imports](${first_release_url}).")
243+
fi
244+
245+
tag_commit="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_REF_NAME}" --jq .sha)"
246+
if [ "$tag_commit" != "$GITHUB_SHA" ]; then
247+
echo "The release tag no longer points to the workflow commit." >&2
248+
exit 1
249+
fi
250+
251+
gh release create "$GITHUB_REF_NAME" \
252+
dist/terraform-provider-kernel_"${version}"_*.zip \
253+
"$manifest" \
254+
"$checksum" \
255+
"$signature" \
256+
--repo "$GITHUB_REPOSITORY" \
257+
--verify-tag \
258+
--draft \
259+
--generate-notes \
260+
"${notes[@]}" \
261+
--title "$GITHUB_REF_NAME"
262+
263+
- name: Publish release
264+
env:
265+
GH_TOKEN: ${{ github.token }}
266+
run: |
267+
tag_commit="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_REF_NAME}" --jq .sha)"
268+
if [ "$tag_commit" != "$GITHUB_SHA" ]; then
269+
echo "The release tag no longer points to the workflow commit." >&2
270+
exit 1
271+
fi
272+
273+
gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft=false

docs/architecture.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Kernel Terraform Provider Architecture
22

3-
This document records the durable-only architecture, the implemented v0 baseline, and the target scope for the first public v1 of the Kernel Terraform provider.
3+
This document records the durable-only architecture, the initial public v0.0.1 surface, and the target scope for a future v1 of the Kernel Terraform provider.
44

55
## First Principles
66

@@ -42,7 +42,7 @@ Import:
4242

4343
## v1 Target Scope
4444

45-
The first public v1 should make durable Kernel configuration production-ready without turning Terraform into a runtime control plane. Core items are release-blocking unless the release notes explicitly defer them with an upstream API or SDK blocker.
45+
A future v1 should broaden production-ready durable Kernel configuration without turning Terraform into a runtime control plane.
4646

4747
Resources require stable identity, refresh, delete, import, and, where applicable, project-scoping and sensitive-state semantics. Data sources require stable identity, deterministic exact lookup, and, where applicable, masked sensitive metadata, pagination, and project scoping. Tooling experiments require deterministic regeneration and must preserve the handwritten lifecycle boundary.
4848

@@ -268,4 +268,4 @@ Release checklist:
268268
- Runtime operations are absent from Terraform resources.
269269
- Import behavior is documented.
270270
- API and SDK blockers are either resolved or explicitly deferred.
271-
- Release process, signing, licensing, and versioning are complete before the first public v1 publication.
271+
- Release process, signing, licensing, and versioning are complete before the first public v0.0.1 publication.

docs/first-release.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# First Public Release
22

3-
The first published Kernel Terraform provider version will be v1. This
3+
The first published Kernel Terraform provider version will be v0.0.1. This
44
repository has no earlier published tags, so this release has no provider
55
upgrade or state migration path.
66

docs/release.md

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Use this checklist before publishing a Kernel Terraform provider version.
44

55
## Release Preconditions
66

7-
- The repository has no published provider tags. Present v1 as the first public release, not as an upgrade or migration from v0.
7+
- The repository has no published provider tags. Present v0.0.1 as the first public release, not as an upgrade or migration from an earlier provider version.
88
- Review the [first public release guide](first-release.md) and include its supported-surface and import guidance in the release notes.
99
- Work from a clean `main` checkout after the PR stack is merged.
1010
- Run `bash scripts/check-docs.sh`.
@@ -28,12 +28,41 @@ Use this checklist before publishing a Kernel Terraform provider version.
2828
- Verify unscoped API calls send no `X-Kernel-Project-Id` header; it is sent only when a resource-level `project_id` or the provider default resolves a project.
2929
- Confirm `terraform-registry-manifest.json` contains protocol `["6.0"]` for Terraform Plugin Framework.
3030
- Confirm `LICENSE` contains the approved Apache License 2.0 text.
31+
- Confirm immutable GitHub Releases are enabled for the repository. The
32+
publication job intentionally has no repository-administration permission to
33+
inspect or change this setting.
3134
- Confirm GitHub private vulnerability reporting or a public security contact is configured and reflected in `SECURITY.md`.
32-
- Confirm there is no branch named like the release tag, for example `v1.0.0`.
35+
- Create a protected GitHub environment named `release`, require approval from
36+
the designated release owners, prevent self-review, and disable administrator
37+
bypass. Add exactly two deployment policies: the `main` branch for manual
38+
preflight and tags matching `v*` for publication. Store
39+
`GPG_PRIVATE_KEY` and `PASSPHRASE` as environment secrets, not repository
40+
secrets. Set the environment variable `GPG_FINGERPRINT` to the fingerprint
41+
registered with the Terraform Registry. Set `RELEASE_REVIEWERS` to the exact
42+
comma-separated reviewer identities in `Type:name` form, such as
43+
`User:octocat,Team:release-owners`. GitHub's environment API does not report
44+
whether administrator bypass is disabled, so verify that setting when
45+
approving a release.
46+
- Add a repository ruleset that restricts creation, update, and deletion of
47+
`v*` tags to the designated release owners. Set the `release` environment
48+
variable `RELEASE_RULESET_ID` to that active ruleset's numeric ID. The workflow
49+
verifies the live environment reviewer identities, deployment policies, and
50+
ruleset controls before
51+
touching signing configuration. Before setting the ID, inspect the ruleset's
52+
bypass list and require explicit release-owner teams or GitHub Apps; do not
53+
allow repository roles or organization administrators to bypass it. GitHub
54+
omits bypass actors from ruleset API responses unless the caller has ruleset
55+
write access, so the least-privilege workflow cannot verify their identities
56+
at runtime.
57+
- GitHub repository writers can create Releases through the API; GitHub does not
58+
provide a separate release-publisher role. Treat every account with repository
59+
write access as release-authorized, keep that group minimal, and use the
60+
protected environment to restrict access to the signing key.
61+
- Confirm there is no branch named like the release tag, for example `v0.0.1`.
3362

3463
## Registry Release Assets
3564

36-
Terraform Registry provider releases are GitHub Releases with semver tags prefixed by `v`, such as `v1.0.0`.
65+
Terraform Registry provider releases are GitHub Releases with semver tags prefixed by `v`, such as `v0.0.1`.
3766

3867
Each release must include:
3968

@@ -61,13 +90,34 @@ Do not replace or mutate assets for a published version. If an asset, checksum,
6190
platforms, checksums, manifest inclusion, and checksum signing.
6291
- Normal CI validates the GoReleaser configuration and registry manifest without
6392
building the complete platform matrix.
64-
- `.github/workflows/release.yml` prepares unsigned, unpublished assets for
65-
stable `vMAJOR.MINOR.PATCH` tags after confirming the repository is public
66-
and the tag commit is reachable from `main`. It pins GoReleaser to the pushed
67-
tag, verifies the release contract, and retains the assets for seven days.
68-
- Real releases sign the checksum file once with the GPG key selected by
69-
`GPG_FINGERPRINT`. The detached signature is named by appending `.sig` to the
70-
checksum filename. Publication remains a separate release step.
93+
- `.github/workflows/release.yml` runs for `v*` tags. Its preparation job has
94+
read-only repository access and accepts only stable `vMAJOR.MINOR.PATCH`
95+
versions. It requires the Apache 2.0 license, public repository visibility,
96+
and a commit reachable from `main`, then builds and verifies the unsigned
97+
assets. The workflow artifact is retained for seven days. Only the protected
98+
publication job receives `contents: write`.
99+
- Before creating a tag, run the workflow manually with the intended version.
100+
Manual runs create an unpushed tag only inside the ephemeral runner, build
101+
the same unsigned assets, and enter the protected `release` environment to
102+
verify its reviewer, tag policy, ruleset, checksum, and GPG signing
103+
configuration. They never create a remote tag or GitHub Release.
104+
- For the manual preflight, inspect the prepared artifact before approving the
105+
protected `release` job. After approval, the job verifies the release
106+
protections, checksums, and GPG signing configuration without publishing.
107+
- For a tag-triggered release, confirm the acceptance matrix passed and the tag
108+
ruleset's bypass list still contains only designated release owners before
109+
approving the protected `release` job. After approval, the job revalidates the
110+
tag and checksums, requires the imported key to match `GPG_FINGERPRINT`, signs
111+
the checksum file, and publishes the GitHub Release.
112+
- Failed-job reruns reuse the prepared artifact from the same workflow run. A
113+
full rerun replaces that run's artifact and requires a fresh environment
114+
approval. If publication fails or is interrupted, it may leave a draft. Any
115+
existing draft stops retries until a release owner inspects and removes it
116+
manually. An existing published release always stops the workflow.
117+
- For `v0.0.1`, GitHub includes the tagged
118+
[first public release guide](first-release.md) with the generated release
119+
notes. Later versions use generated release notes without first-release
120+
guidance.
71121

72122
## Registry Setup
73123

@@ -97,3 +147,4 @@ References:
97147
- HashiCorp Terraform provider publishing: https://developer.hashicorp.com/terraform/registry/providers/publishing
98148
- HashiCorp provider registry protocol: https://developer.hashicorp.com/terraform/internals/provider-registry-protocol
99149
- GoReleaser checksum signing: https://goreleaser.com/customization/sign/
150+
- GitHub deployment environments: https://docs.github.com/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments

0 commit comments

Comments
 (0)