diff --git a/.github/release-rules/checks.json b/.github/release-rules/checks.json new file mode 100644 index 0000000..45bc819 --- /dev/null +++ b/.github/release-rules/checks.json @@ -0,0 +1,61 @@ +{ + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": [ + "refs/heads/main" + ], + "exclude": [] + } + }, + "name": "Gem release checks", + "bypass_actors": [ + { + "actor_id": 5, + "actor_type": "RepositoryRole", + "bypass_mode": "pull_request" + } + ], + "rules": [ + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": true, + "do_not_enforce_on_create": false, + "required_status_checks": [ + { + "context": "3.3 on ubuntu" + }, + { + "context": "3.4 on ubuntu" + }, + { + "context": "4.0 on ubuntu" + }, + { + "context": "check" + }, + { + "context": "ruby on ubuntu" + }, + { + "context": "ruby on macos" + }, + { + "context": "validate" + }, + { + "context": "Test Packages" + }, + { + "context": "generate" + }, + { + "context": "Release validation" + } + ] + } + } + ] +} diff --git a/.github/release-rules/history.json b/.github/release-rules/history.json new file mode 100644 index 0000000..e6f4af1 --- /dev/null +++ b/.github/release-rules/history.json @@ -0,0 +1,22 @@ +{ + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": [ + "refs/heads/main" + ], + "exclude": [] + } + }, + "name": "Gem release history", + "bypass_actors": [], + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + } + ] +} diff --git a/.github/release-rules/reviews.json b/.github/release-rules/reviews.json new file mode 100644 index 0000000..b36fd66 --- /dev/null +++ b/.github/release-rules/reviews.json @@ -0,0 +1,37 @@ +{ + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": [ + "refs/heads/main" + ], + "exclude": [] + } + }, + "name": "Gem release reviews", + "bypass_actors": [ + { + "actor_id": 5, + "actor_type": "RepositoryRole", + "bypass_mode": "pull_request" + } + ], + "rules": [ + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 2, + "dismiss_stale_reviews_on_push": true, + "require_last_push_approval": true, + "required_review_thread_resolution": true, + "require_code_owner_review": false, + "allowed_merge_methods": [ + "merge", + "squash", + "rebase" + ] + } + } + ] +} diff --git a/.github/release-rules/tags.json b/.github/release-rules/tags.json new file mode 100644 index 0000000..8f7a46f --- /dev/null +++ b/.github/release-rules/tags.json @@ -0,0 +1,22 @@ +{ + "name": "Gem release tags", + "target": "tag", + "enforcement": "active", + "bypass_actors": [], + "conditions": { + "ref_name": { + "include": [ + "refs/tags/v*" + ], + "exclude": [] + } + }, + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + } + ] +} diff --git a/.github/workflows/release-prepare.yaml b/.github/workflows/release-prepare.yaml new file mode 100644 index 0000000..1dc024f --- /dev/null +++ b/.github/workflows/release-prepare.yaml @@ -0,0 +1,52 @@ +name: Prepare release + +on: + workflow_dispatch: + inputs: + bump: + description: Version increment + required: true + type: choice + options: [patch, minor, major] + refresh: + description: Preserve and regenerate an existing release branch + type: boolean + default: false + +permissions: + contents: write + pull-requests: write + +concurrency: + group: release-prepare + cancel-in-progress: false + +env: + BUNDLE_WITH: maintenance + +jobs: + prepare: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + ref: main + fetch-depth: 0 + persist-credentials: false + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4" + bundler-cache: true + - name: Create release PR + env: + GITHUB_TOKEN: ${{ github.token }} + BUMP: ${{ inputs.bump }} + REFRESH: ${{ inputs.refresh }} + run: | + # GitHub.com's shared Actions bot ID; this identity is not for GitHub Enterprise Server. + # https://github.com/actions/checkout#push-a-commit-using-the-built-in-token + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + case "$BUMP" in patch|minor|major) ;; *) exit 1 ;; esac + bundle exec bake "gem:github:release:$BUMP" "refresh=$REFRESH" diff --git a/.github/workflows/release-publish.yaml b/.github/workflows/release-publish.yaml new file mode 100644 index 0000000..722aac8 --- /dev/null +++ b/.github/workflows/release-publish.yaml @@ -0,0 +1,103 @@ +name: Publish release + +# Inspect the exact pushed commit and publish only a validated, merged release PR. +on: + push: + branches: ["main"] + +permissions: + contents: read + pull-requests: read + +env: + BUNDLE_WITH: maintenance + +jobs: + inspect: + runs-on: ubuntu-latest + outputs: + release: ${{ steps.inspect.outputs.release }} + commit: ${{ steps.inspect.outputs.commit }} + pull_request: ${{ steps.inspect.outputs.pull_request }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + persist-credentials: false + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4" + bundler-cache: true + - id: inspect + env: + GITHUB_TOKEN: ${{ github.token }} + RELEASE_COMMIT: ${{ github.sha }} + run: bundle exec bake gem:github:release:resolve + + publish: + needs: inspect + if: needs.inspect.outputs.release == 'true' + runs-on: ubuntu-latest + environment: rubygems + concurrency: + group: release-publish + cancel-in-progress: false + queue: max + env: + RELEASE_PR: ${{ needs.inspect.outputs.pull_request }} + permissions: + contents: write + pull-requests: read + actions: read + id-token: write + attestations: write + steps: + - uses: actions/checkout@v7 + with: + # Inspection verified this merged commit belongs to the default branch. + ref: ${{ needs.inspect.outputs.commit }} + fetch-depth: 0 + persist-credentials: false + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4" + rubygems: '4.0.21' + bundler-cache: true + - name: Build or restore artifact + id: build + env: + GITHUB_TOKEN: ${{ github.token }} + GEM_SIGNING_KEY: ${{ secrets.GEM_SIGNING_KEY }} + run: bundle exec bake gem:github:release:build + - name: Sign RubyGems attestation + if: steps.build.outputs.restored != 'true' + env: + PACKAGE: ${{ steps.build.outputs.package }} + run: gem exec sigstore-cli:0.2.3 sign "$PACKAGE" --bundle "$PACKAGE.sigstore.json" + - name: Attest gem and release receipt + if: steps.build.outputs.restored != 'true' + id: attest + uses: actions/attest@v4 + with: + subject-path: | + ${{ steps.build.outputs.package }} + pkg/release.json + - name: Retain provenance bundle + if: steps.build.outputs.restored != 'true' + env: + ATTESTATION_BUNDLE: ${{ steps.attest.outputs.bundle-path }} + run: cp "$ATTESTATION_BUNDLE" pkg/provenance.sigstore.json + - name: Preserve release before upload + if: steps.build.outputs.restored != 'true' + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.build.outputs.artifact }} + path: pkg/ + if-no-files-found: error + retention-days: 90 + - uses: rubygems/configure-rubygems-credentials@main + - name: Verify, publish, and finalize + env: + GITHUB_TOKEN: ${{ github.token }} + run: bundle exec bake gem:github:release:publish diff --git a/.github/workflows/release-validate.yaml b/.github/workflows/release-validate.yaml new file mode 100644 index 0000000..5d6c9f3 --- /dev/null +++ b/.github/workflows/release-validate.yaml @@ -0,0 +1,32 @@ +name: Validate release + +on: + pull_request: + branches: ["main"] + +permissions: + contents: read + +env: + BUNDLE_WITH: maintenance + +jobs: + validate: + name: Release validation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + persist-credentials: false + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4" + bundler-cache: true + - name: Regenerate release content + env: + RELEASE_BASE: ${{ github.event.pull_request.base.sha }} + run: bundle exec bake gem:github:release:validate "base=$RELEASE_BASE" + - name: Build unsigned package + run: bundle exec bake gem:build signing_key=false diff --git a/bake.rb b/bake.rb index 4f989e3..6bcfecb 100644 --- a/bake.rb +++ b/bake.rb @@ -3,17 +3,11 @@ # Released under the MIT License. # Copyright, 2024-2026, by Samuel Williams. -# Update the project documentation with the new version number. +# Update copyrights and project documentation for the new version. # # @parameter version [String] The new version number. def after_gem_release_version_increment(version) + context["modernize:license"].call context["releases:update"].call(version) context["utopia:project:update"].call end - -# Create a GitHub release for the given tag. -# -# @parameter tag [String] The tag to create a release for. -def after_gem_release(tag:, **options) - context["releases:github:release"].call(tag) -end diff --git a/config/release.yaml b/config/release.yaml new file mode 100644 index 0000000..fee83bd --- /dev/null +++ b/config/release.yaml @@ -0,0 +1,19 @@ +--- +schema: 1 +repository: socketry/utopia-project +branch: main +checks: +- 3.3 on ubuntu +- 3.4 on ubuntu +- 4.0 on ubuntu +- check +- ruby on ubuntu +- ruby on macos +- validate +- Test Packages +- generate +- Release validation +approvals: 2 +signing: true +ruby: '3.4' +environment: rubygems diff --git a/gems.rb b/gems.rb index bfb6bed..ef6fb81 100644 --- a/gems.rb +++ b/gems.rb @@ -9,7 +9,7 @@ gemspec group :maintenance, optional: true do - gem "bake-gem" + gem "bake-gem-github", ">= 0.6.0" gem "bake-modernize" gem "bake-releases" diff --git a/readme.md b/readme.md index b34e7d3..a4c2244 100644 --- a/readme.md +++ b/readme.md @@ -33,6 +33,17 @@ Please see the [project documentation](https://socketry.github.io/utopia-project Please see the [project releases](https://socketry.github.io/utopia-project/releases/index) for all releases. +### Unreleased + + - Manage releases through GitHub pull requests and publish signed gems with RubyGems Trusted Publishing and attestations. + - Sort guides by order (defaulting to zero), then name. + - Fix supplemental documentation paths and missing guide/reference responses. + - Handle empty READMEs and guides without descriptions when rendering pages and generating agent context. + - Treat empty Markdown headings as missing titles and preserve the following content when rendering pages or updating documentation. + - Cover all Ruby, task, and rendered template lines, and exercise the generated site in Chromium at mobile and desktop widths in light and dark mode. + - Add padding to documentation table cells and allow tables to scroll whenever they exceed the available width. + - Scale table, inline code, badge, navigation link, and disclosure spacing with the local font size. + ### v0.45.0 - [Web Packages](https://socketry.github.io/utopia-project/releases/index#web-packages) @@ -72,10 +83,6 @@ Please see the [project releases](https://socketry.github.io/utopia-project/rele - Fix schema for `index.yaml` context file. -### v0.34.0 - - - Introduce `bake utopia:project:agent:context:update` command to update the agent context from the guides in the project. - ## See Also - [Utopia](https://github.com/socketry/utopia) — The website framework which powers this web application. @@ -101,12 +108,14 @@ $ bundle exec sus ### Making Releases -To make a new release: +To prepare a release pull request: ``` bash -$ bundle exec bake gem:release:patch # or minor or major +$ bundle exec bake gem:github:release:patch # or minor or major ``` +See [bake-gem-github](https://github.com/socketry/bake-gem-github) for setup and publishing details. + ### Developer Certificate of Origin In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed. diff --git a/release.cert b/release.cert index d98e595..d88bb2f 100644 --- a/release.cert +++ b/release.cert @@ -1,28 +1,24 @@ -----BEGIN CERTIFICATE----- -MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11 -ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK -CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz -MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd -MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj -bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB -igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2 -9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW -sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE -e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN -XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss -RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn -tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM -zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW -xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O -BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs -aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs -aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE -cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl -xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/ -c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp -8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws -JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP -eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt -Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8 -voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg= +MIIEFDCCAnygAwIBAgIBATANBgkqhkiG9w0BAQsFADAwMREwDwYDVQQKDAhTb2Nr +ZXRyeTEbMBkGA1UEAwwSU29ja2V0cnkgUnVieSBHZW1zMB4XDTI2MDkyMTA3NTgx +NFoXDTI3MDkyMTA3NTgxNFowMDERMA8GA1UECgwIU29ja2V0cnkxGzAZBgNVBAMM +ElNvY2tldHJ5IFJ1YnkgR2VtczCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoC +ggGBAM/QgjVgDzDo/xJEQoFvAzcVFP7msnswkhPJB2UDsbxXCZmers5jV512TM5s +NwLHfzJiC4DcI5ax9ZYKM9Q+dS21YYagNqjtg3YPyDqR6phoibEA0VoMuInUQW68 +i5OkCJzYKGD2pYYVZnuFNqyM2ECUvXh/fBmvPoHbncAHhWPaCBH8mQJ8sRNc6+RV +SoZZu1Yo/aW1zQ+SpZwad7s1ZmigfDNHKhJ22KwZHk+/5Zw1QXVfcajjswV0Mm2P +irgjKN3fyNVkwlly63EBlR4VydT+g9QtJtqh7ee0ThhI+v4wYovf4dbLNcEBTHu1 +x1pGbXGx/2fBAST5eVwaXIx1V8VU22AJcBk1H2o5/1F0HfbD2HQjmFOA112KUwBW +9TtspZD3g6rCQFX53XvL9h6j2y1ukl7s5AdEzOe/x6r1MO8tSDtgcYoF89wgF/88 +Q/Eski0FckQ+znfESZcFcpzs2sPDoYW6SNVX4tZcNty85Y6WBHfHKio14x63PeyC +CG1gMwIDAQABozkwNzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQU +SuMc0x24Xshtaa8SQVkBr2Z5WzIwDQYJKoZIhvcNAQELBQADggGBABI62ey5lDWm +j8jsTQaNBMBb7LbKS3XkBwyL1UNGEwlicp399rJDeuNgVSHqBCZ/nE4yleNjgW9Y +N6DKdxXCvXmFUoHvUINeAvThHxmlEGhSfXl1x55xHEig0rEP4jgdjnYdQI4fqeOM +zIDN0F3ruArke4Y62SQgWVN7vspdAA41hBIRl1vsvs0KWG8DIVQ8cmR+TG6zjOmK +iUiUZGNPnStV1O1xW83c+Ba4Am0krP6/forxDPhRvTehqkVYvMdPfYcICNb5IM58 +5vaYvuj5AtfZuZWN0F/0KtSphhNC17rh6h8QmuYdMJ1clvlrkiEaO1UjPVwjgtxC +x/ARh5X6q0/YTXl4r7EfSJEeqW7qhgu9EUj3mGIPHYdX0Dqih/NgK20F3GvQtTfB +7sse0duKBHXONYVwKAsceUVvqtCfyF608bCilCkbhw3QaInRj+BkRQKJ5Tuj2PZR +76a/hwaV2wWn5RtAFBaUQzQdo/xTqJ7IkwaFjOZe/QyvyoRjCK9Rdg== -----END CERTIFICATE----- diff --git a/releases.md b/releases.md index f0cad3e..e2b7955 100644 --- a/releases.md +++ b/releases.md @@ -2,6 +2,7 @@ ## Unreleased + - Manage releases through GitHub pull requests and publish signed gems with RubyGems Trusted Publishing and attestations. - Sort guides by order (defaulting to zero), then name. - Fix supplemental documentation paths and missing guide/reference responses. - Handle empty READMEs and guides without descriptions when rendering pages and generating agent context.