From 81ad0bfb3416f164245bff20a5a00da89a7a8464 Mon Sep 17 00:00:00 2001 From: Josh <118245755+Scriptception@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:10:21 +1000 Subject: [PATCH 1/3] Automate verified provider releases --- .github/workflows/release.yml | 89 +++++++++++++++++++++++++++++++++-- AGENTS.md | 11 +++-- README.md | 3 ++ VERSION | 1 + 4 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 VERSION diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 83d4fa9..539e4ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,39 +1,120 @@ name: release on: - push: - tags: ['v*'] + workflow_run: + workflows: [Tests] + types: [completed] + branches: [main] + workflow_dispatch: permissions: + actions: read contents: write +concurrency: + group: provider-release + cancel-in-progress: false + jobs: goreleaser: + if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event.workflow_run.head_sha || 'main' }} - uses: actions/setup-go@v5 with: go-version: '1.26.5' cache: true - - name: Verify tag is the current main commit + - name: Resolve release version + id: release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WORKFLOW_RUN_SHA: ${{ github.event.workflow_run.head_sha }} run: | - git fetch --no-tags origin main + set -euo pipefail + version="$(tr -d '[:space:]' < VERSION)" + if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "VERSION must contain a semantic version such as 0.2.2" >&2 + exit 1 + fi + grep -Fqx "## ${version}" CHANGELOG.md + + tag="v${version}" + git fetch --force --tags origin main test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" + if [ -n "${WORKFLOW_RUN_SHA}" ]; then + test "${WORKFLOW_RUN_SHA}" = "$(git rev-parse HEAD)" + else + ci_sha="$(gh run list \ + --repo "${GITHUB_REPOSITORY}" \ + --workflow test.yml \ + --branch main \ + --commit "$(git rev-parse HEAD)" \ + --status success \ + --limit 1 \ + --json headSha \ + --jq '.[0].headSha // ""')" + test "${ci_sha}" = "$(git rev-parse HEAD)" + fi + if git rev-parse --verify --quiet "refs/tags/${tag}^{commit}" >/dev/null; then + test "$(git rev-parse "refs/tags/${tag}^{commit}")" = "$(git rev-parse HEAD)" + tag_exists=true + else + tag_exists=false + fi + + release_status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \ + --header 'Accept: application/vnd.github+json' \ + --header "Authorization: Bearer ${GITHUB_TOKEN}" \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${tag}")" + case "${release_status}" in + 200) release_exists=true ;; + 404) release_exists=false ;; + *) + echo "GitHub release lookup failed with HTTP ${release_status}" >&2 + exit 1 + ;; + esac + + printf 'tag=%s\n' "${tag}" >> "${GITHUB_OUTPUT}" + printf 'commit=%s\n' "$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}" + printf 'tag_exists=%s\n' "${tag_exists}" >> "${GITHUB_OUTPUT}" + printf 'release_exists=%s\n' "${release_exists}" >> "${GITHUB_OUTPUT}" - name: Verify source and tests + if: steps.release.outputs.release_exists != 'true' run: | + set -euo pipefail go mod tidy git diff --exit-code -- go.mod go.sum + go build ./... go test ./... - name: Import GPG key + if: steps.release.outputs.release_exists != 'true' uses: crazy-max/ghaction-import-gpg@v6 id: import_gpg with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.PASSPHRASE }} + - name: Create release tag + if: steps.release.outputs.release_exists != 'true' && steps.release.outputs.tag_exists != 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_SHA: ${{ steps.release.outputs.commit }} + RELEASE_TAG: ${{ steps.release.outputs.tag }} + run: | + set -euo pipefail + git tag "${RELEASE_TAG}" "${RELEASE_SHA}" + gh api --method POST \ + "repos/${GITHUB_REPOSITORY}/git/refs" \ + --raw-field "ref=refs/tags/${RELEASE_TAG}" \ + --raw-field "sha=${RELEASE_SHA}" >/dev/null - name: Run GoReleaser + if: steps.release.outputs.release_exists != 'true' uses: goreleaser/goreleaser-action@v6 with: version: latest diff --git a/AGENTS.md b/AGENTS.md index 8586035..74a9e81 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,8 @@ Keep implementation, documentation, examples, tests, commits, and releases gener and a generic example. 3. Regenerate documentation after every schema or example change. 4. Update `docs/api-coverage.md` and `CHANGELOG.md` when coverage or behaviour changes. -5. Run the smallest relevant checks, then the complete release gate before tagging. +5. Run the smallest relevant checks, then the complete release gate before changing + `VERSION` for a release. ```sh gofmt -w @@ -56,8 +57,10 @@ run `make generate`. Never bypass a generated-doc diff. ## Releases -- Use semantic version tags from a tested commit on `main`. -- Update the changelog before tagging. -- Let the tag-triggered GitHub Actions workflow run GoReleaser and create signed assets. +- Keep the exact semantic version in `VERSION` and add the matching changelog heading. +- Merge the tested `VERSION` change to `main`; do not push release tags manually. +- After the exact `main` test workflow succeeds, the release workflow verifies the + commit and version, creates the matching tag, and runs GoReleaser. A manual dispatch + safely retries an interrupted release without moving an existing tag. - Verify the test workflow, release workflow, checksums, signature, manifest, and latest published version before considering a release complete. diff --git a/README.md b/README.md index 26ea015..c04a872 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,9 @@ make fmt # gofmt Read-only acceptance tests require `AIRLOCK_URL`, `AIRLOCK_API_KEY`, and `TF_ACC=1`. Mutation acceptance tests additionally require `AIRLOCK_ACC_MUTATION=1` and should only be run against an isolated Airlock environment with disposable `tf-acc-*` objects. Never commit live Airlock URLs, API keys, hostnames, user details, group names, or response fixtures. See [AGENTS.md](./AGENTS.md) for architecture, safety, validation, and release conventions. +Releases use the exact semantic version in [`VERSION`](./VERSION). After the exact `main` +test workflow succeeds, gated GitHub Actions creates the matching tag and signed +GoReleaser assets when that version has not already been published. ## Contributing diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..ee1372d --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.2.2 From 87d7997da81776b402d7338608ec8da5ea744a8c Mon Sep 17 00:00:00 2001 From: Josh <118245755+Scriptception@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:14:38 +1000 Subject: [PATCH 2/3] Harden release workflow retries --- .github/workflows/release.yml | 11 +++++++++-- .goreleaser.yml | 2 ++ AGENTS.md | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 539e4ab..60cec1e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,11 @@ concurrency: jobs: goreleaser: - if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.event == 'push' && + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_branch == 'main') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -54,6 +58,7 @@ jobs: --workflow test.yml \ --branch main \ --commit "$(git rev-parse HEAD)" \ + --event push \ --status success \ --limit 1 \ --json headSha \ @@ -61,7 +66,6 @@ jobs: test "${ci_sha}" = "$(git rev-parse HEAD)" fi if git rev-parse --verify --quiet "refs/tags/${tag}^{commit}" >/dev/null; then - test "$(git rev-parse "refs/tags/${tag}^{commit}")" = "$(git rev-parse HEAD)" tag_exists=true else tag_exists=false @@ -80,6 +84,9 @@ jobs: exit 1 ;; esac + if [ "${release_exists}" != "true" ] && [ "${tag_exists}" = "true" ]; then + test "$(git rev-parse "refs/tags/${tag}^{commit}")" = "$(git rev-parse HEAD)" + fi printf 'tag=%s\n' "${tag}" >> "${GITHUB_OUTPUT}" printf 'commit=%s\n' "$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}" diff --git a/.goreleaser.yml b/.goreleaser.yml index cd56610..ded9dc1 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -39,6 +39,8 @@ signs: - "${artifact}" release: + use_existing_draft: true + replace_existing_artifacts: true extra_files: - glob: 'terraform-registry-manifest.json' name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' diff --git a/AGENTS.md b/AGENTS.md index 74a9e81..949ac18 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,6 +61,6 @@ run `make generate`. Never bypass a generated-doc diff. - Merge the tested `VERSION` change to `main`; do not push release tags manually. - After the exact `main` test workflow succeeds, the release workflow verifies the commit and version, creates the matching tag, and runs GoReleaser. A manual dispatch - safely retries an interrupted release without moving an existing tag. + safely retries an interrupted current-`main` release without moving an existing tag. - Verify the test workflow, release workflow, checksums, signature, manifest, and latest published version before considering a release complete. From aafb778e264326af9175d15b755e60b164fac12f Mon Sep 17 00:00:00 2001 From: Josh <118245755+Scriptception@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:15:41 +1000 Subject: [PATCH 3/3] Verify published release tags remain present --- .github/workflows/release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 60cec1e..bff16c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,7 +84,9 @@ jobs: exit 1 ;; esac - if [ "${release_exists}" != "true" ] && [ "${tag_exists}" = "true" ]; then + if [ "${release_exists}" = "true" ]; then + test "${tag_exists}" = "true" + elif [ "${tag_exists}" = "true" ]; then test "$(git rev-parse "refs/tags/${tag}^{commit}")" = "$(git rev-parse HEAD)" fi