From e7e2a87320cc927e2dbb95158575c3f6ac32564b Mon Sep 17 00:00:00 2001 From: Ken Odegard Date: Fri, 6 Dec 2024 19:11:05 -0600 Subject: [PATCH 1/6] Add create-release action --- create-release/README.md | 88 +++++++++++++++++++++++++++++ create-release/action.yml | 71 +++++++++++++++++++++++ create-release/get_release_notes.py | 54 ++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 create-release/README.md create mode 100644 create-release/action.yml create mode 100644 create-release/get_release_notes.py diff --git a/create-release/README.md b/create-release/README.md new file mode 100644 index 0000000..97303ae --- /dev/null +++ b/create-release/README.md @@ -0,0 +1,88 @@ +# Create Release + +This action creates a release on GitHub. There are three parts to a release: +1. Archiving the current repository state as a tarball (`.tar.gz`). While GitHub also does this for releases those archives are unfortunately not stable and cannot be relied on. +2. Computing the checksum for the archived tarball. +3. Extracting the release notes from the changelog. + +## GitHub Action Usage + +```yaml +name: Create Release + +on: + workflow_dispatch: + inputs: + version: + description: The release version + required: true + +permissions: + contents: write + +jobs: + create: + runs-on: ubuntu-latest + steps: + - name: Create Release + uses: conda/actions/create-release + with: + # [required] + # the version to be released + version: ${{ inputs.version }} + + # [required] + # the target branch for the release + branch: main + + # [optional] + # name of the tarball archive + # archive-name: ${{ github.event.repository.name }}-${{ github.ref_name }} + + # [optional] + # directory for the release artifacts + # output-directory: release + + # [optional] + # path to the release notes + # release-notes: RELEASE_NOTES.md +``` + +### Sample Workflow Releasing to Dynamic Branch + +```yaml +name: Create Release + +on: + workflow_dispatch: + inputs: + version: + description: The release version + required: true + +permissions: + contents: write + +jobs: + create: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-python + with: + python-version: '>=3.4' + + # derive the branch from the version + - shell: python + run: | + from os import environ + from pathlib import Path + + branch = "${{ inputs.version }}".rsplit(".", 1)[0] + Path(environ["GITHUB_ENV"]).write_text(f"BRANCH={branch}.x") + + - name: Create Release + uses: conda/actions/create-release + with: + version: ${{ inputs.version }} + branch: ${{ env.BRANCH }} +``` diff --git a/create-release/action.yml b/create-release/action.yml new file mode 100644 index 0000000..35c0a2f --- /dev/null +++ b/create-release/action.yml @@ -0,0 +1,71 @@ +name: Create Release +description: Create a release on GitHub +inputs: + version: + description: The release version + required: true + branch: + description: Target branch for the release + required: true + archive-name: + description: Name of the tarball archive + default: ${{ github.event.repository.name }}-${{ github.ref_name }} + output-directory: + description: Directory for the release artifacts + default: release + release-notes: + description: Path to the release notes + default: RELEASE_NOTES.md +runs: + using: composite + steps: + - name: Checkout Source + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Create Release Directory + shell: bash + run: mkdir -p ${{ inputs.output-directory }} + + - name: Archive Source + shell: bash + run: > + git archive + --prefix="${{ inputs.archive-name }}/" + --output="${{ inputs.output-directory }}/${{ inputs.archive-name }}.tar.gz" + HEAD + + - name: Compute Checksum + shell: bash + run: > + sha256sum "${{ inputs.output-directory }}/${{ inputs.archive-name }}.tar.gz" + | awk '{print $1}' + > "${{ inputs.output-directory }}/${{ inputs.archive-name }}.tar.gz.sha256sum" + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '>=3.9' + + - name: Pip List + shell: bash + run: pip list + + - name: Get Release Notes + shell: bash + run: > + python .github/scripts/get_release_notes.py + --input="${{ inputs.changelog }}" + --version="${{ inputs.version }}" + --output="${{ inputs.output-directory }}/${{ inputs.release-notes }}" + + - name: Create Release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: > + gh release create + --notes-file "${{ inputs.output-directory }}/${{ inputs.release-notes }}" + --target "${{ inputs.branch }}" + --title "${{ inputs.version }}" + "${{ inputs.version }}" + ${{ inputs.output-directory }}/${{ env.ARCHIVE_NAME }}.* diff --git a/create-release/get_release_notes.py b/create-release/get_release_notes.py new file mode 100644 index 0000000..32464a7 --- /dev/null +++ b/create-release/get_release_notes.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import re +from pathlib import Path +from argparse import ArgumentParser, ArgumentTypeError + + +def get_input(value: str) -> Path: + path = Path(value) + if not path.exists(): + raise ArgumentTypeError(f"{value!r} does not exist") + return path + + +def get_output(value: str) -> Path: + path = Path(value) + path.parent.mkdir(parents=True, exist_ok=True) + return path + + +def get_version(value: str) -> str: + if not value: + raise ArgumentTypeError("must be a non-empty string") + return value + + +parser = ArgumentParser() +parser.add_argument("--input", required=True, type=get_input) +parser.add_argument("--output", required=True, type=get_output) +parser.add_argument("--version", required=True, type=get_version) +params = parser.parse_args() + +text = params.input.read_text() +pattern = re.compile( + rf""" + \n+ + ( + \#\#\s+ # markdown header + {re.escape(params.version)}\s+ # version number + \(\d\d\d\d-\d\d-\d\d\) # release date + )\n+ + ( + .+? # release notes + )\n+ + ( + \#\#\s+ # markdown header + \d+\.\d+\.\d+\s+ # version number + \(\d\d\d\d-\d\d-\d\d\) # release date + )\n+ + """, + flags=re.VERBOSE | re.DOTALL, +) +notes = match.group(2) if (match := pattern.search(text)) else "" +params.output.write_text(notes) From a4cee6bd8b2e9908e9aabbd6bde22e8339df8918 Mon Sep 17 00:00:00 2001 From: Ken Odegard Date: Mon, 9 Dec 2024 09:57:48 -0600 Subject: [PATCH 2/6] Add token input --- create-release/README.md | 18 ++++++++++++------ create-release/action.yml | 9 +++++++-- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/create-release/README.md b/create-release/README.md index 97303ae..8ddc1bd 100644 --- a/create-release/README.md +++ b/create-release/README.md @@ -7,6 +7,10 @@ This action creates a release on GitHub. There are three parts to a release: ## GitHub Action Usage +Requirements: +- `contents: write` permission to the repository +- (optional) Python >=3.7 + ```yaml name: Create Release @@ -46,6 +50,11 @@ jobs: # [optional] # path to the release notes # release-notes: RELEASE_NOTES.md + + # [optional] + # token for creating the release + # (`contents: write` for fine-grained PAT; `repo` for classic PAT) + # token: ${{ github.token }} ``` ### Sample Workflow Releasing to Dynamic Branch @@ -67,16 +76,13 @@ jobs: create: runs-on: ubuntu-latest steps: - - uses: actions/setup-python - with: - python-version: '>=3.4' - - # derive the branch from the version - - shell: python + - name: Get Branch + shell: python run: | from os import environ from pathlib import Path + # derive the branch from the version by dropping the `PATCH` and using `.x` branch = "${{ inputs.version }}".rsplit(".", 1)[0] Path(environ["GITHUB_ENV"]).write_text(f"BRANCH={branch}.x") diff --git a/create-release/action.yml b/create-release/action.yml index 35c0a2f..771c361 100644 --- a/create-release/action.yml +++ b/create-release/action.yml @@ -16,6 +16,11 @@ inputs: release-notes: description: Path to the release notes default: RELEASE_NOTES.md + token: + description: >- + Token for creating the release + (`contents: write` for fine-grained PAT; `repo` for classic PAT) + default: ${{ github.token }} runs: using: composite steps: @@ -44,7 +49,7 @@ runs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: '>=3.9' + python-version: '>=3.7' - name: Pip List shell: bash @@ -61,7 +66,7 @@ runs: - name: Create Release shell: bash env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ input.token }} run: > gh release create --notes-file "${{ inputs.output-directory }}/${{ inputs.release-notes }}" From d160c782edced2c6d2f7105cdb87ae144c1a84ca Mon Sep 17 00:00:00 2001 From: Ken Odegard Date: Mon, 9 Dec 2024 19:29:28 -0600 Subject: [PATCH 3/6] Cleanup --- create-release/README.md | 10 +++++----- create-release/action.yml | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/create-release/README.md b/create-release/README.md index 8ddc1bd..7e773d0 100644 --- a/create-release/README.md +++ b/create-release/README.md @@ -1,6 +1,6 @@ # Create Release -This action creates a release on GitHub. There are three parts to a release: +This action creates a release on GitHub: 1. Archiving the current repository state as a tarball (`.tar.gz`). While GitHub also does this for releases those archives are unfortunately not stable and cannot be relied on. 2. Computing the checksum for the archived tarball. 3. Extracting the release notes from the changelog. @@ -18,7 +18,7 @@ on: workflow_dispatch: inputs: version: - description: The release version + description: Release version required: true permissions: @@ -52,12 +52,12 @@ jobs: # release-notes: RELEASE_NOTES.md # [optional] - # token for creating the release + # GitHub token to author release # (`contents: write` for fine-grained PAT; `repo` for classic PAT) # token: ${{ github.token }} ``` -### Sample Workflow Releasing to Dynamic Branch +### Sample Workflow Creating Release using Dynamic Branch ```yaml name: Create Release @@ -66,7 +66,7 @@ on: workflow_dispatch: inputs: version: - description: The release version + description: Release version required: true permissions: diff --git a/create-release/action.yml b/create-release/action.yml index 771c361..0488d35 100644 --- a/create-release/action.yml +++ b/create-release/action.yml @@ -1,25 +1,25 @@ name: Create Release -description: Create a release on GitHub +description: Creates a release by archiving the source and creating a release on GitHub. inputs: version: - description: The release version + description: Release version. required: true branch: - description: Target branch for the release + description: Target branch for the release. required: true archive-name: - description: Name of the tarball archive + description: Name of the tarball archive. default: ${{ github.event.repository.name }}-${{ github.ref_name }} output-directory: - description: Directory for the release artifacts + description: Directory for the release artifacts. default: release release-notes: - description: Path to the release notes + description: Path to the release notes. default: RELEASE_NOTES.md token: description: >- - Token for creating the release - (`contents: write` for fine-grained PAT; `repo` for classic PAT) + GitHub token to author release + (`contents: write` for fine-grained PAT; `repo` for classic PAT). default: ${{ github.token }} runs: using: composite From a8533c74283c92b20c9702ad42ce88ea77712d24 Mon Sep 17 00:00:00 2001 From: Ken Odegard Date: Tue, 10 Dec 2024 16:22:54 -0600 Subject: [PATCH 4/6] Correct script path --- create-release/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/create-release/action.yml b/create-release/action.yml index 0488d35..eb18369 100644 --- a/create-release/action.yml +++ b/create-release/action.yml @@ -58,7 +58,7 @@ runs: - name: Get Release Notes shell: bash run: > - python .github/scripts/get_release_notes.py + python ${{ github.action_path }}/get_release_notes.py --input="${{ inputs.changelog }}" --version="${{ inputs.version }}" --output="${{ inputs.output-directory }}/${{ inputs.release-notes }}" From c8dd6aeaa878ac04dc8961f81bbfb0a577b817ea Mon Sep 17 00:00:00 2001 From: Ken Odegard Date: Wed, 11 Dec 2024 19:18:38 -0600 Subject: [PATCH 5/6] Update README.md --- create-release/README.md | 45 ++++++++++++++------------------------- create-release/action.yml | 12 +++++------ 2 files changed, 21 insertions(+), 36 deletions(-) diff --git a/create-release/README.md b/create-release/README.md index 7e773d0..7581d22 100644 --- a/create-release/README.md +++ b/create-release/README.md @@ -5,11 +5,20 @@ This action creates a release on GitHub: 2. Computing the checksum for the archived tarball. 3. Extracting the release notes from the changelog. -## GitHub Action Usage +## Action Inputs -Requirements: -- `contents: write` permission to the repository -- (optional) Python >=3.7 +| Name | Description | Default | +| ---- | ----------- | ------- | +| `version` | Version to release. | **Required** | +| `branch` | Target branch to use for the release. | `${{ github.even.repository.default_branch` | +| `archive-name` | Name of the git archive to create. | `${{ github.event.repository.name }}-${{ inputs.version }}` | +| `output-directory` | Directory for the release artifacts. | `release` | +| `release-notes` | Name of the release notes to create. | `RELEASE_NOTES.md` | +| `token` | GitHub token to create the release.
Fine-grained PAT: `contents: write` | `${{ github.token }}` | + +## Sample Workflows + +### Basic Workflow ```yaml name: Create Release @@ -18,7 +27,7 @@ on: workflow_dispatch: inputs: version: - description: Release version + description: The version to release. required: true permissions: @@ -31,33 +40,11 @@ jobs: - name: Create Release uses: conda/actions/create-release with: - # [required] - # the version to be released version: ${{ inputs.version }} - - # [required] - # the target branch for the release branch: main - - # [optional] - # name of the tarball archive - # archive-name: ${{ github.event.repository.name }}-${{ github.ref_name }} - - # [optional] - # directory for the release artifacts - # output-directory: release - - # [optional] - # path to the release notes - # release-notes: RELEASE_NOTES.md - - # [optional] - # GitHub token to author release - # (`contents: write` for fine-grained PAT; `repo` for classic PAT) - # token: ${{ github.token }} ``` -### Sample Workflow Creating Release using Dynamic Branch +### Dynamic Branch Workflow ```yaml name: Create Release @@ -66,7 +53,7 @@ on: workflow_dispatch: inputs: version: - description: Release version + description: The version to release. required: true permissions: diff --git a/create-release/action.yml b/create-release/action.yml index eb18369..9a6f776 100644 --- a/create-release/action.yml +++ b/create-release/action.yml @@ -2,14 +2,14 @@ name: Create Release description: Creates a release by archiving the source and creating a release on GitHub. inputs: version: - description: Release version. + description: Version to release. required: true branch: description: Target branch for the release. - required: true + default: ${{ github.event.repository.default_branch }} archive-name: - description: Name of the tarball archive. - default: ${{ github.event.repository.name }}-${{ github.ref_name }} + description: Name of the git archive. + default: ${{ github.event.repository.name }}-${{ inputs.version }} output-directory: description: Directory for the release artifacts. default: release @@ -17,9 +17,7 @@ inputs: description: Path to the release notes. default: RELEASE_NOTES.md token: - description: >- - GitHub token to author release - (`contents: write` for fine-grained PAT; `repo` for classic PAT). + description: 'GitHub token to create the release. Fine-grained PAT: `contents: write`' default: ${{ github.token }} runs: using: composite From 8b41367a424e35670f3a21595d4c5976ad57537c Mon Sep 17 00:00:00 2001 From: Ken Odegard Date: Tue, 17 Dec 2024 09:21:41 -0600 Subject: [PATCH 6/6] Add pip cache --- create-release/action.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/create-release/action.yml b/create-release/action.yml index 9a6f776..e3eaff6 100644 --- a/create-release/action.yml +++ b/create-release/action.yml @@ -44,6 +44,13 @@ runs: | awk '{print $1}' > "${{ inputs.output-directory }}/${{ inputs.archive-name }}.tar.gz.sha256sum" + - name: Load Pip Cache + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + with: + path: ~/.cache/pip + # invalidate the cache anytime a workflow changes + key: ${{ github.workflow }}-${{ hashFiles('.github/workflows/*') }} + - name: Setup Python uses: actions/setup-python@v5 with: