From 94a2bf97edaee3e675f1eb90fde0ce433879b4ce Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 09:02:09 +0000 Subject: [PATCH 1/3] ci: release with release-please Replace tag-triggered releases with release-please. Merging the release PR creates a draft release, and the workflow attaches the binaries, checksums and signature before publishing it, since immutable releases reject uploads after publication. Renovate now uses fix(deps) for dependencies that ship in the binary or image, so their updates cut a release. Release-As: 0.8.1 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 138 +++++++++++++++++++--------------- .release-please-manifest.json | 3 + CONTRIBUTING.md | 18 +++++ release-please-config.json | 10 +++ renovate.jsonc | 18 ++++- 5 files changed, 127 insertions(+), 60 deletions(-) create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 229be03..fe4c8c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,10 +2,12 @@ name: Release on: push: - tags: ["v*.*.*"] + branches: [main] pull_request: - paths: [".github/workflows/release.yml"] - workflow_dispatch: + paths: + - .github/workflows/release.yml + - .release-please-manifest.json + - release-please-config.json concurrency: group: release-${{ github.ref }} @@ -14,10 +16,6 @@ concurrency: permissions: {} cache-mode: none -env: - RELEASE_REF: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref_name }} - CHECKSUMS_FILE: decolint_${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}_checksums.txt - # The runner's default shell adds only -e, so an unset variable or a failure # mid-pipeline would otherwise pass silently. defaults: @@ -25,55 +23,69 @@ defaults: shell: bash -euo pipefail {0} jobs: + # Opens the release PR, and creates a draft release once it is merged. The + # draft is published only after every asset is attached, since immutable + # releases reject later uploads. If a later job fails, re-run the failed + # jobs: a fresh run would not create the release again. + # + # The job runs on pull requests too, with its steps skipped, so that the + # jobs after it are not skipped with it. + release-please: + permissions: {} + runs-on: ubuntu-slim + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + steps: + # A GitHub App token, because a release PR opened with GITHUB_TOKEN + # would not trigger CI. + - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + if: github.event_name == 'push' + id: app-token + with: + client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + permission-contents: write + permission-issues: write + permission-pull-requests: write + - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 + if: github.event_name == 'push' + id: release + with: + token: ${{ steps.app-token.outputs.token }} + prepare: - permissions: - contents: read + permissions: {} + needs: release-please + if: github.event_name == 'pull_request' || needs.release-please.outputs.release_created == 'true' runs-on: ubuntu-slim outputs: - skip_build: ${{ steps.decide.outputs.skip_build }} - skip_release: ${{ steps.decide.outputs.skip_release }} + release_ref: ${{ steps.decide.outputs.release_ref }} + tag_name: ${{ needs.release-please.outputs.tag_name }} steps: - - name: Summary - run: | - { - echo "### Release Summary" - echo "- Version: \`$RELEASE_REF\`" - } >> "$GITHUB_STEP_SUMMARY" - - name: Decide release actions + - name: Decide release ref id: decide env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - IS_PULL_REQUEST: ${{ github.event_name == 'pull_request' }} + TAG_NAME: ${{ needs.release-please.outputs.tag_name }} + PR_NUMBER: ${{ github.event.pull_request.number }} run: | - is_semver=false - if [[ "$RELEASE_REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then - is_semver=true - fi - - release_exists=false - if [ "$IS_PULL_REQUEST" != "true" ] && [ "$is_semver" = "true" ] && gh release view "$RELEASE_REF" >/dev/null 2>&1; then - release_exists=true - echo "- Release \`$RELEASE_REF\` already exists. Skipping build." >> "$GITHUB_STEP_SUMMARY" - fi - - skip_release=false - if [ "$IS_PULL_REQUEST" = "true" ] || [ "$is_semver" != "true" ]; then - skip_release=true - echo "- Version \`$RELEASE_REF\` is not a release candidate. Skipping sign/attest/release." >> "$GITHUB_STEP_SUMMARY" - elif [ "$release_exists" = "true" ]; then - skip_release=true + echo "### Release Summary" >> "$GITHUB_STEP_SUMMARY" + if [ -n "$TAG_NAME" ]; then + release_ref=$TAG_NAME + else + release_ref=pr-$PR_NUMBER + echo "- Pull request build. Skipping sign/attest/release." >> "$GITHUB_STEP_SUMMARY" fi - - echo "skip_build=$release_exists" >> "$GITHUB_OUTPUT" - echo "skip_release=$skip_release" >> "$GITHUB_OUTPUT" + echo "- Version: \`$release_ref\`" >> "$GITHUB_STEP_SUMMARY" + echo "release_ref=$release_ref" >> "$GITHUB_OUTPUT" build: permissions: contents: read cache-mode: read needs: prepare - if: needs.prepare.outputs.skip_build != 'true' + env: + RELEASE_REF: ${{ needs.prepare.outputs.release_ref }} strategy: fail-fast: false matrix: @@ -131,7 +143,9 @@ jobs: checksums: permissions: contents: read - needs: build + needs: [build, prepare] + env: + CHECKSUMS_FILE: decolint_${{ needs.prepare.outputs.release_ref }}_checksums.txt runs-on: ubuntu-slim steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -155,7 +169,9 @@ jobs: id-token: write contents: read needs: [checksums, prepare] - if: needs.prepare.outputs.skip_release != 'true' + if: needs.prepare.outputs.tag_name != '' + env: + CHECKSUMS_FILE: decolint_${{ needs.prepare.outputs.release_ref }}_checksums.txt runs-on: ubuntu-latest steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -180,7 +196,9 @@ jobs: attestations: write contents: read needs: [checksums, prepare] - if: needs.prepare.outputs.skip_release != 'true' + if: needs.prepare.outputs.tag_name != '' + env: + CHECKSUMS_FILE: decolint_${{ needs.prepare.outputs.release_ref }}_checksums.txt runs-on: ubuntu-slim steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -198,7 +216,8 @@ jobs: attestations: write cache-mode: read needs: prepare - if: needs.prepare.outputs.skip_build != 'true' + env: + RELEASE_REF: ${{ needs.prepare.outputs.release_ref }} runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -218,11 +237,11 @@ jobs: images: ghcr.io/bare-devcontainer/decolint flavor: latest=auto tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} + type=semver,pattern={{version}},value=${{ needs.prepare.outputs.tag_name }} + type=semver,pattern={{major}}.{{minor}},value=${{ needs.prepare.outputs.tag_name }} + type=semver,pattern={{major}},value=${{ needs.prepare.outputs.tag_name }} - name: Log in to GHCR - if: needs.prepare.outputs.skip_release != 'true' + if: needs.prepare.outputs.tag_name != '' uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io @@ -234,7 +253,7 @@ jobs: with: context: . platforms: linux/amd64,linux/arm64 - push: ${{ needs.prepare.outputs.skip_release != 'true' }} + push: ${{ needs.prepare.outputs.tag_name != '' }} provenance: mode=max sbom: true tags: ${{ steps.meta.outputs.tags }} @@ -244,7 +263,7 @@ jobs: VERSION=${{ env.RELEASE_REF }} REVISION=${{ steps.revision.outputs.short_sha }} - name: Attest image provenance - if: needs.prepare.outputs.skip_release != 'true' + if: needs.prepare.outputs.tag_name != '' uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-name: ghcr.io/bare-devcontainer/decolint @@ -256,7 +275,9 @@ jobs: actions: write contents: write needs: [checksums, sign, attest, docker, prepare] - if: needs.prepare.outputs.skip_release != 'true' + if: needs.prepare.outputs.tag_name != '' + env: + CHECKSUMS_FILE: decolint_${{ needs.prepare.outputs.release_ref }}_checksums.txt runs-on: ubuntu-slim steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -270,14 +291,13 @@ jobs: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: signature-${{ github.run_id }} - - name: Create GitHub Release + - name: Publish GitHub Release env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} - TAG_NAME: ${{ github.ref_name }} + TAG_NAME: ${{ needs.prepare.outputs.tag_name }} run: | - gh release create "$TAG_NAME" \ - dist/* "$CHECKSUMS_FILE" "$CHECKSUMS_FILE.sigstore.json" \ - --title "$TAG_NAME" \ - --generate-notes + gh release upload "$TAG_NAME" --clobber \ + dist/* "$CHECKSUMS_FILE" "$CHECKSUMS_FILE.sigstore.json" + gh release edit "$TAG_NAME" --draft=false gh workflow run attest-check.yml --ref main diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..64f3cdd --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.8.0" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba780aa..d79ff9e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -241,3 +241,21 @@ PR titles must follow the [Conventional Commits](https://www.conventionalcommits.org/) format with one of the types `feat`, `fix`, `ci`, `chore`, `test`, or `docs`, e.g. `feat(cli): add new feature`. Keep external dependencies to a minimum. +The title becomes the commit message on `main`, and its type decides +the next release: `feat` bumps the minor version, `fix` the patch +version, and the other types do not cut a release. Use `fix` for a +dependency update that ships in the binary or image. + +## Releasing + +Releases are cut by +[release-please](https://github.com/googleapis/release-please). It keeps +a release PR open that bumps the version and updates `CHANGELOG.md`; +merging it builds, signs and publishes the release. To release a +version other than the computed one, add a `Release-As: x.y.z` footer +to a commit on `main`. + +release-please runs as a GitHub App, so that CI runs on the release +PR. The App needs read and write access to contents, issues and pull +requests, with its client ID in the `RELEASE_APP_CLIENT_ID` repository +variable and a private key in the `RELEASE_APP_PRIVATE_KEY` secret. diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..daf17a4 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/v17.6.0/schemas/config.json", + "release-type": "go", + "include-component-in-tag": false, + "draft": true, + "force-tag-creation": true, + "packages": { + ".": {} + } +} diff --git a/renovate.jsonc b/renovate.jsonc index ada2ad9..58f8860 100644 --- a/renovate.jsonc +++ b/renovate.jsonc @@ -3,7 +3,8 @@ "extends": [ "config:best-practices", ":semanticCommits", - ":semanticCommitTypeAll(chore)" + // fix(deps) for what ships in the binary and image, so release-please cuts a release for it. + ":semanticPrefixFixDepsChoreOthers" ], "minimumReleaseAge": "14 days", "labels": [ @@ -60,6 +61,21 @@ } ], "packageRules": [ + { + "description": "dockerflagsgen is a code generator; its dependencies do not ship.", + "matchFileNames": [ + "cmd/dockerflagsgen/go.mod" + ], + "semanticCommitType": "chore" + }, + { + "description": "The Go toolchain builds the binary and image, so its updates ship.", + "matchPackageNames": [ + "go", + "golang" + ], + "semanticCommitType": "fix" + }, { "description": "Limit Claude Code updates to one pull request a week, since upstream tags releases several times a day.", "matchPackageNames": [ From 0a742f032e8b8ac196b54de931d9930c6a9885eb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 05:28:36 +0000 Subject: [PATCH 2/3] ci: read release-please App credentials from the release environment Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 9 +++++---- CONTRIBUTING.md | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fe4c8c4..2720186 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,10 +28,11 @@ jobs: # releases reject later uploads. If a later job fails, re-run the failed # jobs: a fresh run would not create the release again. # - # The job runs on pull requests too, with its steps skipped, so that the - # jobs after it are not skipped with it. + # The job runs on pull requests too, with its steps skipped and outside the + # release environment, so that the jobs after it are not skipped with it. release-please: permissions: {} + environment: ${{ github.event_name == 'push' && 'release' || '' }} runs-on: ubuntu-slim outputs: release_created: ${{ steps.release.outputs.release_created }} @@ -43,8 +44,8 @@ jobs: if: github.event_name == 'push' id: app-token with: - client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + client-id: ${{ vars.RELEASE_PLEASE_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_PLEASE_PRIVATE_KEY }} permission-contents: write permission-issues: write permission-pull-requests: write diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d79ff9e..f026890 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -257,5 +257,6 @@ to a commit on `main`. release-please runs as a GitHub App, so that CI runs on the release PR. The App needs read and write access to contents, issues and pull -requests, with its client ID in the `RELEASE_APP_CLIENT_ID` repository -variable and a private key in the `RELEASE_APP_PRIVATE_KEY` secret. +requests, with its client ID in the `RELEASE_PLEASE_CLIENT_ID` variable +and a private key in the `RELEASE_PLEASE_PRIVATE_KEY` secret of the +`release` environment. From 86ea19c3ab5a056cf59e2d0584a7a20c0443cc8e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 05:36:40 +0000 Subject: [PATCH 3/3] ci: re-run the release workflow after allowing release-please-action Co-Authored-By: Claude Opus 5.5