Skip to content

Commit 203eeab

Browse files
authored
Gate signed provider publication (#98)
Require protected release approval before importing the Registry key, signing the prepared checksum, and publishing retry-safe GitHub Release assets.
1 parent 712dfa5 commit 203eeab

11 files changed

Lines changed: 326 additions & 48 deletions

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ jobs:
5757
- name: Check generated docs
5858
run: bash scripts/check-docs.sh
5959

60+
- name: Test release scripts
61+
run: bash scripts/test-release-scripts.sh
62+
6063
- name: Set up GoReleaser
6164
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
6265
with:

.github/workflows/release.yml

Lines changed: 127 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
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+
type: string
713

814
permissions:
915
contents: read
1016

17+
env:
18+
RELEASE_TAG: ${{ inputs.version || github.ref_name }}
19+
20+
defaults:
21+
run:
22+
shell: bash
23+
1124
concurrency:
1225
group: release-${{ github.ref }}
1326
cancel-in-progress: false
@@ -31,8 +44,13 @@ jobs:
3144
run: |
3245
test -s LICENSE
3346
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
47+
if [[ ! "$RELEASE_TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
48+
echo "Release versions must use stable semantic version format vMAJOR.MINOR.PATCH." >&2
49+
exit 1
50+
fi
51+
52+
if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ] && [ "$GITHUB_REF" != "refs/heads/main" ]; then
53+
echo "Manual release preflight must run from main." >&2
3654
exit 1
3755
fi
3856
@@ -47,6 +65,15 @@ jobs:
4765
exit 1
4866
fi
4967
68+
- name: Create local preflight tag
69+
if: github.event_name == 'workflow_dispatch'
70+
run: |
71+
if git show-ref --verify --quiet "refs/tags/${RELEASE_TAG}"; then
72+
echo "Release version ${RELEASE_TAG} already exists." >&2
73+
exit 1
74+
fi
75+
git tag "$RELEASE_TAG" "$GITHUB_SHA"
76+
5077
- name: Set up Go
5178
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
5279
with:
@@ -60,18 +87,26 @@ jobs:
6087
version: v2.17.0
6188
args: release --clean --skip=publish,sign
6289
env:
63-
GORELEASER_CURRENT_TAG: ${{ github.ref_name }}
90+
GORELEASER_CURRENT_TAG: ${{ env.RELEASE_TAG }}
6491

6592
- name: Verify release artifacts
6693
run: |
67-
version="${GITHUB_REF_NAME#v}"
94+
version="${RELEASE_TAG#v}"
6895
cp terraform-registry-manifest.json "dist/terraform-provider-kernel_${version}_manifest.json"
6996
bash scripts/check-release-artifacts.sh "$version"
7097
98+
- name: Verify signing configuration
99+
if: github.event_name == 'workflow_dispatch'
100+
env:
101+
EXPECTED_GPG_FINGERPRINT: ${{ vars.GPG_FINGERPRINT }}
102+
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
103+
PASSPHRASE: ${{ secrets.PASSPHRASE }}
104+
run: bash scripts/sign-release-checksum.sh "${RELEASE_TAG#v}"
105+
71106
- name: Upload release artifacts
72107
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
73108
with:
74-
name: release-${{ github.ref_name }}-${{ github.run_id }}
109+
name: release-${{ env.RELEASE_TAG }}-${{ github.run_id }}
75110
path: |
76111
dist/*.zip
77112
dist/*_manifest.json
@@ -80,3 +115,89 @@ jobs:
80115
retention-days: 7
81116
compression-level: 0
82117
overwrite: true
118+
119+
publish:
120+
name: Sign and publish release
121+
if: github.event_name == 'push'
122+
needs: prepare
123+
runs-on: ubuntu-latest
124+
timeout-minutes: 10
125+
permissions:
126+
contents: write
127+
128+
steps:
129+
- name: Checkout
130+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
131+
with:
132+
persist-credentials: false
133+
134+
- name: Download release artifacts
135+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
136+
with:
137+
name: release-${{ github.ref_name }}-${{ github.run_id }}
138+
path: dist
139+
140+
- name: Check publication preconditions
141+
env:
142+
GH_TOKEN: ${{ github.token }}
143+
run: |
144+
release="$(
145+
gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/releases?per_page=100" |
146+
jq -c --arg tag "$GITHUB_REF_NAME" '[.[][] | select(.tag_name == $tag)][0] // empty'
147+
)"
148+
if [ -n "$release" ]; then
149+
if [ "$(jq -r .draft <<<"$release")" = "true" ]; then
150+
echo "An existing draft release must be inspected and removed before retrying ${GITHUB_REF_NAME}." >&2
151+
else
152+
echo "A published release already exists for ${GITHUB_REF_NAME}." >&2
153+
fi
154+
exit 1
155+
fi
156+
157+
version="${GITHUB_REF_NAME#v}"
158+
checksum="dist/terraform-provider-kernel_${version}_SHA256SUMS"
159+
test -s "$checksum"
160+
(cd dist && sha256sum --check "$(basename "$checksum")")
161+
162+
- name: Sign and verify checksums
163+
env:
164+
EXPECTED_GPG_FINGERPRINT: ${{ vars.GPG_FINGERPRINT }}
165+
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
166+
PASSPHRASE: ${{ secrets.PASSPHRASE }}
167+
run: bash scripts/sign-release-checksum.sh "${GITHUB_REF_NAME#v}"
168+
169+
- name: Create draft release
170+
env:
171+
GH_TOKEN: ${{ github.token }}
172+
run: |
173+
version="${GITHUB_REF_NAME#v}"
174+
first_release_url="https://github.com/${GITHUB_REPOSITORY}/blob/${GITHUB_SHA}/docs/first-release.md"
175+
manifest="dist/terraform-provider-kernel_${version}_manifest.json"
176+
checksum="dist/terraform-provider-kernel_${version}_SHA256SUMS"
177+
signature="${checksum}.sig"
178+
notes=()
179+
if [ "$GITHUB_REF_NAME" = "v0.0.1" ]; then
180+
notes=(--notes "First-release guidance: [supported surface and imports](${first_release_url}).")
181+
fi
182+
183+
bash scripts/check-release-tag.sh "$GITHUB_REF_NAME" "$GITHUB_SHA"
184+
185+
gh release create "$GITHUB_REF_NAME" \
186+
dist/terraform-provider-kernel_"${version}"_*.zip \
187+
"$manifest" \
188+
"$checksum" \
189+
"$signature" \
190+
--repo "$GITHUB_REPOSITORY" \
191+
--verify-tag \
192+
--draft \
193+
--generate-notes \
194+
"${notes[@]}" \
195+
--title "$GITHUB_REF_NAME"
196+
197+
- name: Publish release
198+
env:
199+
GH_TOKEN: ${{ github.token }}
200+
run: |
201+
bash scripts/check-release-tag.sh "$GITHUB_REF_NAME" "$GITHUB_SHA"
202+
203+
gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft=false

.goreleaser.yml

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,5 @@ checksum:
4545
- glob: terraform-registry-manifest.json
4646
name_template: "{{ .ProjectName }}_{{ .Version }}_manifest.json"
4747

48-
signs:
49-
- artifacts: checksum
50-
signature: "${artifact}.sig"
51-
args:
52-
- --batch
53-
- --local-user
54-
- "{{ .Env.GPG_FINGERPRINT }}"
55-
- --output
56-
- ${signature}
57-
- --detach-sign
58-
- ${artifact}
59-
60-
release:
61-
extra_files:
62-
- glob: terraform-registry-manifest.json
63-
name_template: "{{ .ProjectName }}_{{ .Version }}_manifest.json"
64-
6548
changelog:
6649
disable: true

CHANGELOG.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
All notable changes to the Kernel Terraform provider are recorded here.
44

5-
## Unreleased
5+
## v0.0.1
66

7-
First public release candidate:
7+
First public release:
88

99
- Provider configuration for `api_key`, `base_url`, and `project_id`.
1010
- `kernel_project` and `kernel_browser_pool` resources for durable desired state.
@@ -20,6 +20,5 @@ Intentionally not included:
2020
- API key, profile, proxy, extension, deployment, or app resources.
2121
- `force_destroy` browser-pool deletion.
2222

23-
There is no upgrade or migration path from an earlier published version because
24-
this repository has no published provider tags. See the
25-
[first public release guide](docs/first-release.md).
23+
v0.0.1 has no upgrade or migration path from an earlier published version. See
24+
the [first public release guide](docs/first-release.md).

SECURITY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
## Supported Versions
44

5-
The provider has no public release yet; the first public release ships as a complete v1, and v0 tags stay internal. Before the first public release, confirm which released versions receive security fixes and update this section if support differs from latest-only.
5+
Security fixes are provided for the latest released version. Before the first
6+
release, this policy applies to the `main` branch.
67

78
## Reporting Security Issues
89

docs/architecture.md

Lines changed: 6 additions & 4 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

@@ -25,8 +25,9 @@ Provider configuration:
2525
- optional `base_url`
2626
- optional `project_id`
2727

28-
Resource:
28+
Resources:
2929

30+
- `kernel_project`
3031
- `kernel_browser_pool`
3132

3233
Data sources:
@@ -38,11 +39,12 @@ Data sources:
3839

3940
Import:
4041

42+
- `kernel_project` imports by canonical project ID.
4143
- `kernel_browser_pool` imports by canonical browser pool ID, optionally qualified as `<project-id>/<pool-id>`.
4244

4345
## v1 Target Scope
4446

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.
47+
A future v1 should broaden production-ready durable Kernel configuration without turning Terraform into a runtime control plane.
4648

4749
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.
4850

@@ -268,4 +270,4 @@ Release checklist:
268270
- Runtime operations are absent from Terraform resources.
269271
- Import behavior is documented.
270272
- 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.
273+
- 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 is 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: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ 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.
8-
- Review the [first public release guide](first-release.md) and include its supported-surface and import guidance in the release notes.
7+
- For v0.0.1, present it as the first public release, not as an upgrade or migration from an earlier provider version.
8+
- For v0.0.1, 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`.
1111
- Run `bash scripts/check-markdown-links.sh`.
@@ -28,12 +28,26 @@ 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`.
33-
35+
- Store `GPG_PRIVATE_KEY` and `PASSPHRASE` as repository Actions secrets. Set
36+
the repository Actions variable `GPG_FINGERPRINT` to the fingerprint
37+
registered with the Terraform Registry.
38+
- Add a repository ruleset that restricts creation, update, and deletion of
39+
`v*` tags to the Kernel engineering team. Inspect the ruleset's bypass list
40+
before releasing; do not allow repository roles, outside collaborators, or
41+
organization administrators to bypass it. Ruleset configuration is an
42+
administrator-owned setup requirement, not a workflow runtime check.
43+
- GitHub repository writers can create Releases through the API; GitHub does not
44+
provide a separate release-publisher role. Treat every account with repository
45+
write access as release-authorized and keep that group limited to Kernel
46+
engineers. The tag ruleset remains the control that authorizes a release
47+
workflow run.
3448
## Registry Release Assets
3549

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

3852
Each release must include:
3953

@@ -58,16 +72,34 @@ Do not replace or mutate assets for a published version. If an asset, checksum,
5872
## GoReleaser Notes
5973

6074
- `.goreleaser.yml` is the source of truth for registry artifact names, target
61-
platforms, checksums, manifest inclusion, and checksum signing.
75+
platforms, checksums, and manifest inclusion. The release workflow owns
76+
checksum signing and publication.
6277
- Normal CI validates the GoReleaser configuration and registry manifest without
6378
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.
79+
- `.github/workflows/release.yml` runs for `v*` tags. Its preparation job has
80+
read-only repository access and accepts only stable `vMAJOR.MINOR.PATCH`
81+
versions. It requires the Apache 2.0 license, public repository visibility,
82+
and a commit reachable from `main`, then builds and verifies the unsigned
83+
assets. The workflow artifact is retained for seven days. Only the
84+
tag-triggered publication job receives `contents: write`.
85+
- Before creating a tag, run the workflow manually with the intended version.
86+
Manual runs create an unpushed tag only inside the ephemeral runner, build
87+
the same unsigned assets, and exercise checksum and GPG signing with
88+
`contents: read`. They never create a remote tag or GitHub Release.
89+
- For a tag-triggered release, confirm the acceptance matrix passed and the tag
90+
ruleset's bypass list still contains only the Kernel engineering team before
91+
creating the tag. The job revalidates the tag and checksums, requires the
92+
imported key to match `GPG_FINGERPRINT`, signs the checksum file, and publishes
93+
the GitHub Release.
94+
- Failed-job reruns reuse the prepared artifact from the same workflow run. A
95+
full rerun replaces that run's artifact. If publication fails or is
96+
interrupted, it may leave a draft. Any existing draft stops retries until a
97+
Kernel engineer inspects and removes it manually. An existing published
98+
release always stops the workflow.
99+
- For `v0.0.1`, GitHub includes the tagged
100+
[first public release guide](first-release.md) with the generated release
101+
notes. Later versions use generated release notes without first-release
102+
guidance.
71103

72104
## Registry Setup
73105

@@ -96,4 +128,4 @@ References:
96128

97129
- HashiCorp Terraform provider publishing: https://developer.hashicorp.com/terraform/registry/providers/publishing
98130
- HashiCorp provider registry protocol: https://developer.hashicorp.com/terraform/internals/provider-registry-protocol
99-
- GoReleaser checksum signing: https://goreleaser.com/customization/sign/
131+
- GitHub rulesets: https://docs.github.com/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets

0 commit comments

Comments
 (0)