diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..e332cdc --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,21 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-release-config.json +changelog: + exclude: + labels: + - skip-for-release-notes + categories: + - title: Breaking Changes + labels: + - breaking-change + - title: Enhancements + labels: + - enhancement + - title: Bug Fixes + labels: + - bug + - title: Dependencies + labels: + - dependencies + - title: Other Changes + labels: + - '*' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 33d1d03..1a9e0fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,5 +54,13 @@ jobs: run: pnpm build - name: Verify committed build output - if: ${{ inputs.verify_dist }} + if: >- + ${{ + inputs.verify_dist || + ( + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository && + startsWith(github.head_ref, 'release/') + ) + }} run: git diff --exit-code diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..e707eef --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,138 @@ +name: Publish Release + +on: + push: + branches: + - master + paths: + - 'package.json' + +concurrency: + group: release + cancel-in-progress: false + +permissions: + contents: read + +jobs: + prepare: + if: github.event.repository.fork == false + runs-on: ubuntu-latest + outputs: + should_release: ${{ steps.version.outputs.should_release }} + tag: ${{ steps.version.outputs.tag }} + version: ${{ steps.version.outputs.version }} + major_tag: ${{ steps.version.outputs.major_tag }} + previous_tag: ${{ steps.version.outputs.previous_tag }} + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ github.sha }} + + - name: Setup Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 24.14.0 + + - name: Resolve release version + id: version + run: | + VERSION="$(node --print "require('./package.json').version")" + + if ! printf '%s' "${VERSION}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "package.json version must use x.y.z semver; received ${VERSION}." + exit 1 + fi + + TAG="v${VERSION}" + MAJOR_TAG="${TAG%%.*}" + PREVIOUS_TAG="$(git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*')" + + echo "version=${VERSION}" >> "${GITHUB_OUTPUT}" + echo "tag=${TAG}" >> "${GITHUB_OUTPUT}" + echo "major_tag=${MAJOR_TAG}" >> "${GITHUB_OUTPUT}" + echo "previous_tag=${PREVIOUS_TAG}" >> "${GITHUB_OUTPUT}" + + if git rev-parse "${TAG}" >/dev/null 2>&1; then + echo 'Release tag already exists. Skipping release.' + echo "should_release=false" >> "${GITHUB_OUTPUT}" + exit 0 + fi + + if [ -z "${PREVIOUS_TAG}" ]; then + echo 'Expected an existing previous release tag.' + exit 1 + fi + + PREVIOUS_VERSION="${PREVIOUS_TAG#v}" + + if [ "$(printf '%s\n%s\n' "${PREVIOUS_VERSION}" "${VERSION}" | sort -V | sed -n '$p')" != "${VERSION}" ]; then + echo "package.json version ${VERSION} must be greater than the latest release tag ${PREVIOUS_TAG}." + exit 1 + fi + + echo "should_release=true" >> "${GITHUB_OUTPUT}" + + test: + if: ${{ needs.prepare.outputs.should_release == 'true' }} + needs: prepare + uses: ./.github/workflows/test.yml + with: + ref: ${{ github.sha }} + permissions: + contents: read + + build: + if: ${{ needs.prepare.outputs.should_release == 'true' }} + needs: prepare + uses: ./.github/workflows/build.yml + with: + ref: ${{ github.sha }} + verify_dist: true + permissions: + contents: read + + release: + if: ${{ needs.prepare.outputs.should_release == 'true' }} + needs: [prepare, test, build] + runs-on: ubuntu-latest + permissions: + contents: write + env: + GIT_AUTHOR_NAME: github-actions[bot] + GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ github.sha }} + + - name: Create and push release tag + env: + TAG: ${{ needs.prepare.outputs.tag }} + run: | + git tag -a "${TAG}" -m "Release ${TAG}" "${GITHUB_SHA}" + git push origin "refs/tags/${TAG}" + + - name: Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + PREVIOUS_TAG: ${{ needs.prepare.outputs.previous_tag }} + TAG: ${{ needs.prepare.outputs.tag }} + run: | + gh release create "${TAG}" --verify-tag --generate-notes --notes-start-tag "${PREVIOUS_TAG}" + + - name: Update floating major tag + env: + MAJOR_TAG: ${{ needs.prepare.outputs.major_tag }} + RELEASE_SHA: ${{ github.sha }} + run: | + git tag -fa "${MAJOR_TAG}" -m "Release ${MAJOR_TAG}" "${RELEASE_SHA}" + git push origin "refs/tags/${MAJOR_TAG}" --force diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index b440646..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: Release - -on: - workflow_dispatch: - inputs: - version: - description: Semver tag to release, for example v1.2.3 - required: true - type: string - -concurrency: - group: release - cancel-in-progress: false - -permissions: - contents: read - -jobs: - test: - uses: ./.github/workflows/test.yml - with: - ref: master - permissions: - contents: read - - build: - uses: ./.github/workflows/build.yml - with: - ref: master - verify_dist: true - permissions: - contents: read - - release: - needs: [test, build] - runs-on: ubuntu-latest - permissions: - contents: write - env: - GIT_AUTHOR_NAME: github-actions[bot] - GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com - GIT_COMMITTER_NAME: github-actions[bot] - GIT_COMMITTER_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: master - fetch-depth: 0 - - - name: Validate release input - env: - VERSION: ${{ inputs.version }} - run: | - if ! printf '%s' "${VERSION}" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then - echo 'Version must use semver and start with v, for example v1.2.3.' - exit 1 - fi - - if git rev-parse "${VERSION}" >/dev/null 2>&1; then - echo "Tag ${VERSION} already exists." - exit 1 - fi - - - name: Create and push release tag - env: - VERSION: ${{ inputs.version }} - run: | - PREVIOUS_TAG="$(git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' 2>/dev/null || true)" - echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> "${GITHUB_ENV}" - - git tag -a "${VERSION}" -m "Release ${VERSION}" - git push origin "refs/tags/${VERSION}" - - - name: Create GitHub release - env: - VERSION: ${{ inputs.version }} - GH_TOKEN: ${{ github.token }} - run: | - if [ -n "${PREVIOUS_TAG}" ]; then - gh release create "${VERSION}" --verify-tag --generate-notes --notes-start-tag "${PREVIOUS_TAG}" - else - gh release create "${VERSION}" --verify-tag --generate-notes - fi - - - name: Update floating major tag - env: - VERSION: ${{ inputs.version }} - run: | - MAJOR_TAG="${VERSION%%.*}" - RELEASE_SHA="$(git rev-parse HEAD)" - - git tag -fa "${MAJOR_TAG}" -m "Release ${MAJOR_TAG}" "${RELEASE_SHA}" - git push origin "refs/tags/${MAJOR_TAG}" --force diff --git a/.github/workflows/update-dist.yml b/.github/workflows/update-dist.yml index 8368c10..9d3462b 100644 --- a/.github/workflows/update-dist.yml +++ b/.github/workflows/update-dist.yml @@ -4,9 +4,19 @@ on: push: branches: - master + pull_request: + branches: + - master + paths: + - 'package.json' + types: + - opened + - synchronize + - reopened + - ready_for_review concurrency: - group: update-dist-${{ github.ref }} + group: update-dist-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true permissions: @@ -14,14 +24,28 @@ permissions: jobs: update-dist: - if: github.actor != 'github-actions[bot]' + if: >- + ${{ + github.actor != 'github-actions[bot]' && + ( + ( + github.event_name == 'push' && + github.event.repository.fork == false + ) || + ( + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository && + startsWith(github.head_ref, 'release/') && + contains(fromJSON('["MEMBER", "OWNER"]'), github.event.pull_request.author_association) + ) + ) + }} runs-on: ubuntu-latest - steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: master + ref: ${{ github.event_name == 'pull_request' && github.head_ref || 'master' }} fetch-depth: 0 - name: Setup pnpm @@ -41,11 +65,30 @@ jobs: - name: Commit updated dist output env: - GIT_AUTHOR_NAME: github-actions[bot] + BASE_REF: ${{ github.event.pull_request.base.ref || '' }} + HEAD_REF: ${{ github.head_ref || '' }} + EVENT_NAME: ${{ github.event_name }} GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com - GIT_COMMITTER_NAME: github-actions[bot] + GIT_AUTHOR_NAME: github-actions[bot] GIT_COMMITTER_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] run: | + read_package_version_from_ref() { + git show "$1:package.json" | jq -r '.version' + } + + if [ "${EVENT_NAME}" = 'pull_request' ]; then + git fetch origin "${BASE_REF}" + + VERSION="$(read_package_version_from_ref 'HEAD')" + BASE_VERSION="$(read_package_version_from_ref "origin/${BASE_REF}")" + + if [ "${VERSION}" = "${BASE_VERSION}" ]; then + echo "Pull request does not bump package.json version. Skipping dist update." + exit 0 + fi + fi + if git diff --quiet; then echo "No dist updates to commit." exit 0 @@ -61,4 +104,9 @@ jobs: git diff --name-only -z | xargs -0 git add -- git commit -m "build: update dist for ${SOURCE_SHA}" - git push origin HEAD:master + + if [ "${EVENT_NAME}" = 'pull_request' ]; then + git push origin "HEAD:${HEAD_REF}" + else + git push origin HEAD:master + fi diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..744f25b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +Release notes are published on the GitHub releases page: + + diff --git a/README.md b/README.md index d3d6090..f9c6a98 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,8 @@ directly from the repository ref they pin to. In this repository, `dist/` is tre artifact: - Pull requests are reviewed as source changes and must build successfully. +- Trusted same-repo `release/**` pull requests that bump the root `package.json` version auto-update + committed `dist/` output on each push. - `master` auto-updates committed `dist/` output after merges. - Releases rebuild and fail if a fresh build would change committed output, so release tags always point to commits with up-to-date `dist/`. @@ -119,4 +121,17 @@ The build treats any **top-level directory** that contains an `action.yml` as an ### Release tags and floating majors -Releases are created via the `Release` workflow with a `version` like `v3.0.0`. In addition to creating the `vX.Y.Z` tag and GitHub release, it **force-updates** the floating major tag (for example `v3`). +The root [`package.json`](package.json) version is the release source of truth for this repository. + +To prepare a release: + +1. Open a same-repo pull request with a branch in the form of `release/**` that bumps the root `package.json` `version` field. +2. Let CI auto-update committed `dist/` output on the release pull request as you push changes. +3. Merge the pull request to `master`. + +After merge, the release workflow: + +- reads the merged package version and creates the matching `vX.Y.Z` tag +- generates release notes automatically with GitHub +- uses [`.github/release.yml`](.github/release.yml) labels and categories to section the release page +- force-updates the floating major tag (for example `v3`) diff --git a/package.json b/package.json index 0485826..b260203 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "elastic-github-actions", - "version": "3.0.0", + "version": "2.1.2", "description": "Elastic Organization GitHub Actions", "repository": { "type": "git",