Compare commits

..

No commits in common. "master" and "v0.1.10" have entirely different histories.

20 changed files with 4852 additions and 4942 deletions

View File

@ -1,33 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
groups:
npm:
patterns:
- "*"
ignore:
- dependency-name: node-fetch
versions:
- ">=3.0.0"
# ignore mime and @types/mime per https://github.com/softprops/action-gh-release/pull/475
- dependency-name: mime
versions:
- ">=4.0.0"
- dependency-name: "@types/mime"
versions:
- ">=4.0.0"
commit-message:
prefix: "chore(deps)"
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
groups:
github-actions:
patterns:
- "*"
commit-message:
prefix: "chore(deps)"

22
.github/release.yml vendored
View File

@ -1,22 +0,0 @@
changelog:
exclude:
labels:
- ignore-for-release
- github-actions
authors:
- octocat
- renovate[bot]
categories:
- title: Breaking Changes 🛠
labels:
- breaking-change
- title: Exciting New Features 🎉
labels:
- enhancement
- feature
- title: Bug fixes 🐛
labels:
- bug
- title: Other Changes 🔄
labels:
- "*"

View File

@ -1,20 +1,14 @@
name: main name: Main
on: on: [pull_request, push]
push:
pull_request:
jobs: jobs:
build: build:
runs-on: ubuntu-24.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 # https://github.com/actions/checkout
- name: Checkout
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4 uses: actions/checkout@v2
with:
node-version-file: ".tool-versions"
cache: "npm"
- name: Install - name: Install
run: npm ci run: npm ci
- name: Build - name: Build

View File

@ -1 +0,0 @@
nodejs 20.15.1

View File

@ -1,134 +1,3 @@
## 2.2.1
## What's Changed
### Bug fixes 🐛
* fix: big file uploads by @xen0n in https://github.com/softprops/action-gh-release/pull/562
### Other Changes 🔄
* chore(deps): bump @types/node from 22.10.1 to 22.10.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/559
* chore(deps): bump @types/node from 22.10.2 to 22.10.5 by @dependabot in https://github.com/softprops/action-gh-release/pull/569
* chore: update error and warning messages for not matching files in files field by @ytimocin in https://github.com/softprops/action-gh-release/pull/568
## 2.2.0
## What's Changed
### Exciting New Features 🎉
* feat: read the release assets asynchronously by @xen0n in https://github.com/softprops/action-gh-release/pull/552
### Bug fixes 🐛
* fix(docs): clarify the default for tag_name by @alexeagle in https://github.com/softprops/action-gh-release/pull/544
### Other Changes 🔄
* chore(deps): bump typescript from 5.6.3 to 5.7.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/548
* chore(deps): bump @types/node from 22.9.0 to 22.9.4 by @dependabot in https://github.com/softprops/action-gh-release/pull/547
* chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 by @dependabot in https://github.com/softprops/action-gh-release/pull/545
* chore(deps): bump @vercel/ncc from 0.38.2 to 0.38.3 by @dependabot in https://github.com/softprops/action-gh-release/pull/543
* chore(deps): bump prettier from 3.3.3 to 3.4.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/550
* chore(deps): bump @types/node from 22.9.4 to 22.10.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/551
* chore(deps): bump prettier from 3.4.1 to 3.4.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/554
## 2.1.0
## What's Changed
### Exciting New Features 🎉
* feat: add support for release assets with multiple spaces within the name by @dukhine in https://github.com/softprops/action-gh-release/pull/518
* feat: preserve upload order by @richarddd in https://github.com/softprops/action-gh-release/pull/500
### Other Changes 🔄
* chore(deps): bump @types/node from 22.8.2 to 22.8.7 by @dependabot in https://github.com/softprops/action-gh-release/pull/539
## 2.0.9
- maintenance release with updated dependencies
## 2.0.8
### Other Changes 🔄
* chore(deps): bump prettier from 2.8.0 to 3.3.3 by @dependabot in https://github.com/softprops/action-gh-release/pull/480
* chore(deps): bump @types/node from 20.14.9 to 20.14.11 by @dependabot in https://github.com/softprops/action-gh-release/pull/483
* chore(deps): bump @octokit/plugin-throttling from 9.3.0 to 9.3.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/484
* chore(deps): bump glob from 10.4.2 to 11.0.0 by @dependabot in https://github.com/softprops/action-gh-release/pull/477
* refactor: write jest config in ts by @chenrui333 in https://github.com/softprops/action-gh-release/pull/485
* chore(deps): bump @actions/github from 5.1.1 to 6.0.0 by @dependabot in https://github.com/softprops/action-gh-release/pull/470
## 2.0.7
### Bug fixes 🐛
* Fix missing update release body by @FirelightFlagboy in https://github.com/softprops/action-gh-release/pull/365
### Other Changes 🔄
* Bump @octokit/plugin-retry from 4.0.3 to 7.1.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/443
* Bump typescript from 4.9.5 to 5.5.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/467
* Bump @types/node from 20.14.6 to 20.14.8 by @dependabot in https://github.com/softprops/action-gh-release/pull/469
* Bump @types/node from 20.14.8 to 20.14.9 by @dependabot in https://github.com/softprops/action-gh-release/pull/473
* Bump typescript from 5.5.2 to 5.5.3 by @dependabot in https://github.com/softprops/action-gh-release/pull/472
* Bump ts-jest from 29.1.5 to 29.2.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/479
* docs: document that existing releases are updated by @jvanbruegge in https://github.com/softprops/action-gh-release/pull/474
## 2.0.6
- maintenance release with updated dependencies
## 2.0.5
- Factor in file names with spaces when upserting files [#446](https://github.com/softprops/action-gh-release/pull/446) via [@MystiPanda](https://github.com/MystiPanda)
- Improvements to error handling [#449](https://github.com/softprops/action-gh-release/pull/449) via [@till](https://github.com/till)
## 2.0.4
- Minor follow up to [#417](https://github.com/softprops/action-gh-release/pull/417). [#425](https://github.com/softprops/action-gh-release/pull/425)
## 2.0.3
- Declare `make_latest` as an input field in `action.yml` [#419](https://github.com/softprops/action-gh-release/pull/419)
## 2.0.2
- Revisit approach to [#384](https://github.com/softprops/action-gh-release/pull/384) making unresolved pattern failures opt-in [#417](https://github.com/softprops/action-gh-release/pull/417)
## 2.0.1
- Add support for make_latest property [#304](https://github.com/softprops/action-gh-release/pull/304) via [@samueljseay](https://github.com/samueljseay)
- Fail run if files setting contains invalid patterns [#384](https://github.com/softprops/action-gh-release/pull/384) via [@rpdelaney](https://github.com/rpdelaney)
- Add support for proxy env variables (don't use node-fetch) [#386](https://github.com/softprops/action-gh-release/pull/386/) via [@timor-raiman](https://github.com/timor-raiman)
- Suppress confusing warning when input_files is empty [#389](https://github.com/softprops/action-gh-release/pull/389) via [@Drowze](https://github.com/Drowze)
## 2.0.0
- `2.0.0`!? this release corrects a disjunction between git tag versions used in the marketplace and versions list this file. Previous versions should have really been 1.\*. Going forward this should be better aligned.
- Upgrade action.yml declaration to node20 to address deprecations
## 0.1.15
- Upgrade to action.yml declaration to node16 to address deprecations
- Upgrade dependencies
- Add `asset` output as a JSON array containing information about the uploaded assets
## 0.1.14
- provides an new workflow input option `generate_release_notes` which when set to true will automatically generate release notes for you based on GitHub activity [#179](https://github.com/softprops/action-gh-release/pull/179). Please see the [GitHub docs for this feature](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) for more information
## 0.1.13
- fix issue with multiple runs concatenating release bodies [#145](https://github.com/softprops/action-gh-release/pull/145)
## 0.1.12
- fix bug leading to empty strings subsituted for inputs users don't provide breaking api calls [#144](https://github.com/softprops/action-gh-release/pull/144)
## 0.1.11
- better error message on release create failed [#143](https://github.com/softprops/action-gh-release/pull/143)
## 0.1.10 ## 0.1.10
- fixed error message formatting for file uploads - fixed error message formatting for file uploads

View File

@ -1,4 +1,4 @@
Copyright (c) 2019-current Doug Tangren Copyright (c) 2019 Doug Tangren
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

View File

@ -21,16 +21,6 @@
<br /> <br />
- [🤸 Usage](#-usage)
- [🚥 Limit releases to pushes to tags](#-limit-releases-to-pushes-to-tags)
- [⬆️ Uploading release assets](#-uploading-release-assets)
- [📝 External release notes](#-external-release-notes)
- [💅 Customizing](#-customizing)
- [inputs](#inputs)
- [outputs](#outputs)
- [environment variables](#environment-variables)
- [Permissions](#permissions)
## 🤸 Usage ## 🤸 Usage
### 🚥 Limit releases to pushes to tags ### 🚥 Limit releases to pushes to tags
@ -51,10 +41,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
if: github.ref_type == 'tag' if: startsWith(github.ref, 'refs/tags/')
``` ```
You can also use push config tag filter You can also use push config tag filter
@ -72,9 +62,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
``` ```
### ⬆️ Uploading release assets ### ⬆️ Uploading release assets
@ -85,7 +75,6 @@ GitHub release and all are optional.
A common case for GitHub releases is to upload your binary after its been validated and packaged. A common case for GitHub releases is to upload your binary after its been validated and packaged.
Use the `with.files` input to declare a newline-delimited list of glob expressions matching the files Use the `with.files` input to declare a newline-delimited list of glob expressions matching the files
you wish to upload to GitHub releases. If you'd like you can just list the files by name directly. you wish to upload to GitHub releases. If you'd like you can just list the files by name directly.
If a tag already has a GitHub release, the existing release will be updated with the release assets.
Below is an example of uploading a single asset named `Release.txt` Below is an example of uploading a single asset named `Release.txt`
@ -99,14 +88,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Build - name: Build
run: echo ${{ github.sha }} > Release.txt run: echo ${{ github.sha }} > Release.txt
- name: Test - name: Test
run: cat Release.txt run: cat Release.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
if: github.ref_type == 'tag' if: startsWith(github.ref, 'refs/tags/')
with: with:
files: Release.txt files: Release.txt
``` ```
@ -123,23 +112,21 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Build - name: Build
run: echo ${{ github.sha }} > Release.txt run: echo ${{ github.sha }} > Release.txt
- name: Test - name: Test
run: cat Release.txt run: cat Release.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
if: github.ref_type == 'tag' if: startsWith(github.ref, 'refs/tags/')
with: with:
files: | files: |
Release.txt Release.txt
LICENSE LICENSE
``` ```
> **⚠️ Note:** Notice the `|` in the yaml syntax above ☝️. That lets you effectively declare a multi-line yaml string. You can learn more about multi-line yaml syntax [here](https://yaml-multiline.info) > **⚠️ Note:** Notice the `|` in the yaml syntax above ☝️. That let's you effectively declare a multi-line yaml string. You can learn more about multi-line yaml syntax [here](https://yaml-multiline.info)
> **⚠️ Note for Windows:** Paths must use `/` as a separator, not `\`, as `\` is used to escape characters with special meaning in the pattern; for example, instead of specifying `D:\Foo.txt`, you must specify `D:/Foo.txt`. If you're using PowerShell, you can do this with `$Path = $Path -replace '\\','/'`
### 📝 External release notes ### 📝 External release notes
@ -157,18 +144,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Generate Changelog - name: Generate Changelog
run: echo "# Good things have arrived" > ${{ github.workspace }}-CHANGELOG.txt run: echo "# Good things have arrived" > ${{ github.workspace }}-CHANGELOG.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
if: github.ref_type == 'tag' if: startsWith(github.ref, 'refs/tags/')
with: with:
body_path: ${{ github.workspace }}-CHANGELOG.txt body_path: ${{ github.workspace }}-CHANGELOG.txt
repository: my_gh_org/my_gh_repo
# note you'll typically need to create a personal access token # note you'll typically need to create a personal access token
# with permissions to create releases in the other repo # with permissions to create releases in the other repo
token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
env:
GITHUB_REPOSITORY: my_gh_org/my_gh_repo
``` ```
### 💅 Customizing ### 💅 Customizing
@ -178,23 +166,19 @@ jobs:
The following are optional as `step.with` keys The following are optional as `step.with` keys
| Name | Type | Description | | Name | Type | Description |
| -------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `body` | String | Text communicating notable changes in this release | | `body` | String | Text communicating notable changes in this release |
| `body_path` | String | Path to load text communicating notable changes in this release | | `body_path` | String | Path to load text communicating notable changes in this release |
| `draft` | Boolean | Indicator of whether or not this release is a draft | | `draft` | Boolean | Indicator of whether or not this release is a draft |
| `prerelease` | Boolean | Indicator of whether or not is a prerelease | | `prerelease` | Boolean | Indicator of whether or not is a prerelease |
| `preserve_order` | Boolean | Indicator of whether order of files should be preserved when uploading assets |
| `files` | String | Newline-delimited globs of paths to assets to upload for release | | `files` | String | Newline-delimited globs of paths to assets to upload for release |
| `name` | String | Name of the release. defaults to tag name | | `name` | String | Name of the release. defaults to tag name |
| `tag_name` | String | Name of a tag. defaults to `github.ref_name` | | `tag_name` | String | Name of a tag. defaults to `github.ref` |
| `fail_on_unmatched_files` | Boolean | Indicator of whether to fail if any of the `files` globs match nothing | | `fail_on_unmatched_files` | Boolean | Indicator of whether to fail if any of the `files` globs match nothing |
| `repository` | String | Name of a target repository in `<owner>/<repo>` format. Defaults to GITHUB_REPOSITORY env variable | | `repository` | String | Name of a target repository in `<owner>/<repo>` format. Defaults to GITHUB_REPOSITORY env variable |
| `target_commitish` | String | Commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Defaults to repository default branch. | | `target_commitish` | String | Commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. |
| `token` | String | Secret GitHub Personal Access Token. Defaults to `${{ github.token }}` | | `token` | String | Secret GitHub Personal Access Token. Defaults to `${{ github.token }}` |
| `discussion_category_name` | String | If specified, a discussion of the specified category is created and linked to the release. The value must be a category that already exists in the repository. For more information, see ["Managing categories for discussions in your repository."](https://docs.github.com/en/discussions/managing-discussions-for-your-community/managing-categories-for-discussions-in-your-repository) | | `discussion_category_name` | String | If specified, a discussion of the specified category is created and linked to the release. The value must be a category that already exists in the repository. For more information, see ["Managing categories for discussions in your repository."](https://docs.github.com/en/discussions/managing-discussions-for-your-community/managing-categories-for-discussions-in-your-repository) |
| `generate_release_notes` | Boolean | Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes. See the [GitHub docs for this feature](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) for more information |
| `append_body` | Boolean | Append to existing body instead of overwriting it |
| `make_latest` | String | Specifies whether this release should be set as the latest release for the repository. Drafts and prereleases cannot be set as latest. Can be `true`, `false`, or `legacy`. Uses GitHub api defaults if not provided |
💡 When providing a `body` and `body_path` at the same time, `body_path` will be 💡 When providing a `body` and `body_path` at the same time, `body_path` will be
attempted first, then falling back on `body` if the path can not be read from. attempted first, then falling back on `body` if the path can not be read from.
@ -208,13 +192,10 @@ release will retain its original info.
The following outputs can be accessed via `${{ steps.<step-id>.outputs }}` from this action The following outputs can be accessed via `${{ steps.<step-id>.outputs }}` from this action
| Name | Type | Description | | Name | Type | Description |
| ------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------ | ------ | --------------------------------------- |
| `url` | String | Github.com URL for the release | | `url` | String | Github.com URL for the release |
| `id` | String | Release ID | | `id` | String | Release ID |
| `upload_url` | String | URL for uploading assets to the release | | `upload_url` | String | URL for uploading assets to the release |
| `assets` | String | JSON array containing information about each uploaded asset, in the format given [here](https://docs.github.com/en/rest/releases/assets#get-a-release-asset) (minus the `uploader` field) |
As an example, you can use `${{ fromJSON(steps.<step-id>.outputs.assets)[0].browser_download_url }}` to get the download URL of the first asset.
#### environment variables #### environment variables
@ -227,26 +208,4 @@ The following `step.env` keys are allowed as a fallback but deprecated in favor
> **⚠️ Note:** This action was previously implemented as a Docker container, limiting its use to GitHub Actions Linux virtual environments only. With recent releases, we now support cross platform usage. You'll need to remove the `docker://` prefix in these versions > **⚠️ Note:** This action was previously implemented as a Docker container, limiting its use to GitHub Actions Linux virtual environments only. With recent releases, we now support cross platform usage. You'll need to remove the `docker://` prefix in these versions
### Permissions
This Action requires the following permissions on the GitHub integration token:
```yaml
permissions:
contents: write
```
When used with `discussion_category_name`, additional permission is needed:
```yaml
permissions:
contents: write
discussions: write
```
[GitHub token permissions](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) can be set for an individual job, workflow, or for Actions as a whole.
Note that if you intend to run workflows on the release event (`on: { release: { types: [published] } }`), you need to use
a personal access token for this action, as the [default `secrets.GITHUB_TOKEN` does not trigger another workflow](https://github.com/actions/create-release/issues/71).
Doug Tangren (softprops) 2019 Doug Tangren (softprops) 2019

View File

@ -1,5 +1,6 @@
//import * as assert from "assert";
//const assert = require('assert');
import * as assert from "assert"; import * as assert from "assert";
import { text } from "stream/consumers";
import { mimeOrDefault, asset } from "../src/github"; import { mimeOrDefault, asset } from "../src/github";
describe("github", () => { describe("github", () => {
@ -14,10 +15,11 @@ describe("github", () => {
describe("asset", () => { describe("asset", () => {
it("derives asset info from a path", async () => { it("derives asset info from a path", async () => {
const { name, mime, size } = asset("tests/data/foo/bar.txt"); const { name, mime, size, data } = asset("tests/data/foo/bar.txt");
assert.equal(name, "bar.txt"); assert.equal(name, "bar.txt");
assert.equal(mime, "text/plain"); assert.equal(mime, "text/plain");
assert.equal(size, 10); assert.equal(size, 10);
assert.equal(data.toString(), "release me");
}); });
}); });
}); });

View File

@ -5,19 +5,18 @@ import {
parseConfig, parseConfig,
parseInputFiles, parseInputFiles,
unmatchedPatterns, unmatchedPatterns,
uploadUrl, uploadUrl
alignAssetName,
} from "../src/util"; } from "../src/util";
import * as assert from "assert"; import * as assert from "assert";
describe("util", () => { describe("util", () => {
describe("uploadUrl", () => { describe("uploadUrl", () => {
it("strips template", () => { it("stripts template", () => {
assert.equal( assert.equal(
uploadUrl( uploadUrl(
"https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}", "https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}"
), ),
"https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets", "https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets"
); );
}); });
}); });
@ -31,7 +30,7 @@ describe("util", () => {
it("parses newline and comma-delimited (and then some)", () => { it("parses newline and comma-delimited (and then some)", () => {
assert.deepStrictEqual( assert.deepStrictEqual(
parseInputFiles("foo,bar\nbaz,boom,\n\ndoom,loom "), parseInputFiles("foo,bar\nbaz,boom,\n\ndoom,loom "),
["foo", "bar", "baz", "boom", "doom", "loom"], ["foo", "bar", "baz", "boom", "doom", "loom"]
); );
}); });
}); });
@ -47,15 +46,12 @@ describe("util", () => {
input_body_path: undefined, input_body_path: undefined,
input_draft: false, input_draft: false,
input_prerelease: false, input_prerelease: false,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: undefined, input_discussion_category_name: undefined
input_generate_release_notes: false, })
input_make_latest: undefined,
}),
); );
}); });
it("uses input body path", () => { it("uses input body path", () => {
@ -69,15 +65,12 @@ describe("util", () => {
input_body_path: "__tests__/release.txt", input_body_path: "__tests__/release.txt",
input_draft: false, input_draft: false,
input_prerelease: false, input_prerelease: false,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: undefined, input_discussion_category_name: undefined
input_generate_release_notes: false, })
input_make_latest: undefined,
}),
); );
}); });
it("defaults to body path when both body and body path are provided", () => { it("defaults to body path when both body and body path are provided", () => {
@ -91,161 +84,101 @@ describe("util", () => {
input_body_path: "__tests__/release.txt", input_body_path: "__tests__/release.txt",
input_draft: false, input_draft: false,
input_prerelease: false, input_prerelease: false,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: undefined, input_discussion_category_name: undefined
input_generate_release_notes: false, })
input_make_latest: undefined,
}),
); );
}); });
}); });
describe("parseConfig", () => { describe("parseConfig", () => {
it("parses basic config", () => { it("parses basic config", () => {
assert.deepStrictEqual( assert.deepStrictEqual(parseConfig({}), {
parseConfig({
// note: inputs declared in actions.yml, even when declared not required,
// are still provided by the actions runtime env as empty strings instead of
// the normal absent env value one would expect. this breaks things
// as an empty string !== undefined in terms of what we pass to the api
// so we cover that in a test case here to ensure undefined values are actually
// resolved as undefined and not empty strings
INPUT_TARGET_COMMITISH: "",
INPUT_DISCUSSION_CATEGORY_NAME: "",
}),
{
github_ref: "", github_ref: "",
github_repository: "", github_repository: "",
github_token: "", github_token: "",
input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
input_draft: undefined, input_draft: undefined,
input_prerelease: undefined, input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_fail_on_unmatched_files: false, input_fail_on_unmatched_files: false,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: undefined, input_discussion_category_name: undefined
input_generate_release_notes: false, });
input_make_latest: undefined,
},
);
}); });
it("parses basic config with commitish", () => { it("parses basic config with commitish", () => {
assert.deepStrictEqual( assert.deepStrictEqual(
parseConfig({ parseConfig({
INPUT_TARGET_COMMITISH: "affa18ef97bc9db20076945705aba8c516139abd", INPUT_TARGET_COMMITISH: "affa18ef97bc9db20076945705aba8c516139abd"
}), }),
{ {
github_ref: "", github_ref: "",
github_repository: "", github_repository: "",
github_token: "", github_token: "",
input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
input_draft: undefined, input_draft: undefined,
input_prerelease: undefined, input_prerelease: undefined,
input_files: [], input_files: [],
input_preserve_order: undefined,
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_fail_on_unmatched_files: false, input_fail_on_unmatched_files: false,
input_target_commitish: "affa18ef97bc9db20076945705aba8c516139abd", input_target_commitish: "affa18ef97bc9db20076945705aba8c516139abd",
input_discussion_category_name: undefined, input_discussion_category_name: undefined
input_generate_release_notes: false, }
input_make_latest: undefined,
},
); );
}); });
it("supports discussion category names", () => { it("supports discussion category names", () => {
assert.deepStrictEqual( assert.deepStrictEqual(
parseConfig({ parseConfig({
INPUT_DISCUSSION_CATEGORY_NAME: "releases", INPUT_DISCUSSION_CATEGORY_NAME: "releases"
}), }),
{ {
github_ref: "", github_ref: "",
github_repository: "", github_repository: "",
github_token: "", github_token: "",
input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
input_draft: undefined, input_draft: undefined,
input_prerelease: undefined, input_prerelease: undefined,
input_files: [], input_files: [],
input_preserve_order: undefined,
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_fail_on_unmatched_files: false, input_fail_on_unmatched_files: false,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: "releases", input_discussion_category_name: "releases"
input_generate_release_notes: false, }
input_make_latest: undefined,
},
); );
}); });
it("supports generating release notes", () => {
assert.deepStrictEqual(
parseConfig({
INPUT_GENERATE_RELEASE_NOTES: "true",
}),
{
github_ref: "",
github_repository: "",
github_token: "",
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
input_draft: undefined,
input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [],
input_name: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: true,
input_make_latest: undefined,
},
);
});
it("prefers GITHUB_TOKEN over token input for backwards compatibility", () => { it("prefers GITHUB_TOKEN over token input for backwards compatibility", () => {
assert.deepStrictEqual( assert.deepStrictEqual(
parseConfig({ parseConfig({
INPUT_DRAFT: "false", INPUT_DRAFT: "false",
INPUT_PRERELEASE: "true", INPUT_PRERELEASE: "true",
INPUT_PRESERVE_ORDER: "true",
GITHUB_TOKEN: "env-token", GITHUB_TOKEN: "env-token",
INPUT_TOKEN: "input-token", INPUT_TOKEN: "input-token"
}), }),
{ {
github_ref: "", github_ref: "",
github_repository: "", github_repository: "",
github_token: "env-token", github_token: "env-token",
input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
input_draft: false, input_draft: false,
input_prerelease: true, input_prerelease: true,
input_preserve_order: true,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_fail_on_unmatched_files: false, input_fail_on_unmatched_files: false,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: undefined, input_discussion_category_name: undefined
input_generate_release_notes: false, }
input_make_latest: undefined,
},
); );
}); });
it("uses input token as the source of GITHUB_TOKEN by default", () => { it("uses input token as the source of GITHUB_TOKEN by default", () => {
@ -253,106 +186,46 @@ describe("util", () => {
parseConfig({ parseConfig({
INPUT_DRAFT: "false", INPUT_DRAFT: "false",
INPUT_PRERELEASE: "true", INPUT_PRERELEASE: "true",
INPUT_TOKEN: "input-token", INPUT_TOKEN: "input-token"
}), }),
{ {
github_ref: "", github_ref: "",
github_repository: "", github_repository: "",
github_token: "input-token", github_token: "input-token",
input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
input_draft: false, input_draft: false,
input_prerelease: true, input_prerelease: true,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_fail_on_unmatched_files: false, input_fail_on_unmatched_files: false,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: undefined, input_discussion_category_name: undefined
input_generate_release_notes: false, }
input_make_latest: undefined,
},
); );
}); });
it("parses basic config with draft and prerelease", () => { it("parses basic config with draft and prerelease", () => {
assert.deepStrictEqual( assert.deepStrictEqual(
parseConfig({ parseConfig({
INPUT_DRAFT: "false", INPUT_DRAFT: "false",
INPUT_PRERELEASE: "true", INPUT_PRERELEASE: "true"
}), }),
{ {
github_ref: "", github_ref: "",
github_repository: "", github_repository: "",
github_token: "", github_token: "",
input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
input_draft: false, input_draft: false,
input_prerelease: true, input_prerelease: true,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_fail_on_unmatched_files: false, input_fail_on_unmatched_files: false,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: undefined, input_discussion_category_name: undefined
input_generate_release_notes: false, }
input_make_latest: undefined,
},
);
});
it("parses basic config where make_latest is passed", () => {
assert.deepStrictEqual(
parseConfig({
INPUT_MAKE_LATEST: "false",
}),
{
github_ref: "",
github_repository: "",
github_token: "",
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
input_draft: undefined,
input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [],
input_name: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
input_make_latest: "false",
},
);
});
it("parses basic config with append_body", () => {
assert.deepStrictEqual(
parseConfig({
INPUT_APPEND_BODY: "true",
}),
{
github_ref: "",
github_repository: "",
github_token: "",
input_append_body: true,
input_body: undefined,
input_body_path: undefined,
input_draft: undefined,
input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [],
input_name: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
input_make_latest: undefined,
},
); );
}); });
}); });
@ -369,7 +242,7 @@ describe("util", () => {
it("resolves files given a set of paths", async () => { it("resolves files given a set of paths", async () => {
assert.deepStrictEqual( assert.deepStrictEqual(
paths(["tests/data/**/*", "tests/data/does/not/exist/*"]), paths(["tests/data/**/*", "tests/data/does/not/exist/*"]),
["tests/data/foo/bar.txt"], ["tests/data/foo/bar.txt"]
); );
}); });
}); });
@ -378,24 +251,8 @@ describe("util", () => {
it("returns the patterns that don't match any files", async () => { it("returns the patterns that don't match any files", async () => {
assert.deepStrictEqual( assert.deepStrictEqual(
unmatchedPatterns(["tests/data/**/*", "tests/data/does/not/exist/*"]), unmatchedPatterns(["tests/data/**/*", "tests/data/does/not/exist/*"]),
["tests/data/does/not/exist/*"], ["tests/data/does/not/exist/*"]
); );
}); });
}); });
describe("replaceSpacesWithDots", () => {
it("replaces all spaces with dots", () => {
expect(alignAssetName("John Doe.bla")).toBe("John.Doe.bla");
});
it("handles names with multiple spaces", () => {
expect(alignAssetName("John William Doe.bla")).toBe(
"John.William.Doe.bla",
);
});
it("returns the same string if there are no spaces", () => {
expect(alignAssetName("JohnDoe")).toBe("JohnDoe");
});
});
}); });

View File

@ -13,7 +13,7 @@ inputs:
description: "Gives the release a custom name. Defaults to tag name" description: "Gives the release a custom name. Defaults to tag name"
required: false required: false
tag_name: tag_name:
description: "Gives a tag name. Defaults to github.ref_name" description: "Gives a tag name. Defaults to github.GITHUB_REF"
required: false required: false
draft: draft:
description: "Creates a draft release. Defaults to false" description: "Creates a draft release. Defaults to false"
@ -21,9 +21,6 @@ inputs:
prerelease: prerelease:
description: "Identify the release as a prerelease. Defaults to false" description: "Identify the release as a prerelease. Defaults to false"
required: false required: false
preserve_order:
description: "Preserver the order of the artifacts when uploading"
required: false
files: files:
description: "Newline-delimited list of path globs for asset files to upload" description: "Newline-delimited list of path globs for asset files to upload"
required: false required: false
@ -43,17 +40,8 @@ inputs:
discussion_category_name: discussion_category_name:
description: "If specified, a discussion of the specified category is created and linked to the release. The value must be a category that already exists in the repository. If there is already a discussion linked to the release, this parameter is ignored." description: "If specified, a discussion of the specified category is created and linked to the release. The value must be a category that already exists in the repository. If there is already a discussion linked to the release, this parameter is ignored."
required: false required: false
generate_release_notes:
description: "Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes."
required: false
append_body:
description: "Append to existing body instead of overwriting it. Default is false."
required: false
make_latest:
description: "Specifies whether this release should be set as the latest release for the repository. Drafts and prereleases cannot be set as latest. Can be `true`, `false`, or `legacy`. Uses GitHub api default if not provided"
required: false
env: env:
GITHUB_TOKEN: "As provided by Github Actions" "GITHUB_TOKEN": "As provided by Github Actions"
outputs: outputs:
url: url:
description: "URL to the Release HTML Page" description: "URL to the Release HTML Page"
@ -61,10 +49,8 @@ outputs:
description: "Release ID" description: "Release ID"
upload_url: upload_url:
description: "URL for uploading assets to the release" description: "URL for uploading assets to the release"
assets:
description: "JSON array containing information about each uploaded asset, in the format given [here](https://docs.github.com/en/rest/reference/repos#upload-a-release-asset--code-samples) (minus the `uploader` field)"
runs: runs:
using: "node20" using: "node12"
main: "dist/index.js" main: "dist/index.js"
branding: branding:
color: "green" color: "green"

4
dist/index.js vendored

File diff suppressed because one or more lines are too long

11
jest.config.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}

View File

@ -1,16 +0,0 @@
import type { JestConfigWithTsJest } from 'ts-jest';
const config: JestConfigWithTsJest = {
preset: 'ts-jest/presets/default-esm',
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest',
},
verbose: true,
};
export default config;

9633
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,45 +1,45 @@
{ {
"name": "action-gh-release", "name": "action-gh-release",
"version": "2.2.1", "version": "0.1.10",
"private": true, "private": true,
"description": "GitHub Action for creating GitHub Releases", "description": "GitHub Action for creating GitHub Releases",
"main": "lib/main.js", "main": "lib/main.js",
"scripts": { "scripts": {
"build": "ncc build src/main.ts --minify", "build": "ncc build src/main.ts --minify",
"build-debug": "ncc build src/main.ts --v8-cache --source-map",
"test": "jest", "test": "jest",
"fmt": "prettier --write \"src/**/*.ts\" \"__tests__/**/*.ts\"", "fmt": "prettier --write \"src/**/*.ts\" \"__tests__/**/*.ts\"",
"fmtcheck": "prettier --check \"src/**/*.ts\" \"__tests__/**/*.ts\"", "fmtcheck": "prettier --check \"src/**/*.ts\" \"__tests__/**/*.ts\""
"updatetag": "git tag -d v2 && git push origin :v2 && git tag -a v2 -m '' && git push origin v2"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/softprops/action-gh-release.git" "url": "git+https://github.com/softprops/action-gh-template.git"
}, },
"keywords": [ "keywords": [
"actions" "actions"
], ],
"author": "softprops", "author": "softprops",
"license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.11.1", "@actions/core": "^1.4.0",
"@actions/github": "^6.0.0", "@actions/github": "^5.0.0",
"@octokit/plugin-retry": "^7.2.0", "@octokit/plugin-retry": "^3.0.9",
"@octokit/plugin-throttling": "^9.6.1", "@octokit/plugin-throttling": "^3.5.1",
"glob": "^11.0.1", "glob": "^7.1.6",
"mime": "^3.0.0" "mime": "^2.4.4",
"node-fetch": "^2.6.1"
}, },
"devDependencies": { "devDependencies": {
"@types/glob": "^8.1.0", "@types/glob": "^7.1.1",
"@types/jest": "^29.5.14", "@types/jest": "^24.0.25",
"@types/mime": "^3.0.1", "@types/mime": "^2.0.1",
"@types/node": "^22.14.0", "@types/node": "^12.12.24",
"@vercel/ncc": "^0.38.3", "@types/node-fetch": "^2.5.12",
"jest": "^29.3.1", "@zeit/ncc": "^0.21.0",
"jest-circus": "^29.3.1", "jest": "^24.9.0",
"prettier": "3.5.3", "jest-circus": "^24.9.0",
"ts-jest": "^29.3.1", "prettier": "1.19.1",
"ts-node": "^10.9.2", "ts-jest": "^24.2.0",
"typescript": "^5.8.3", "typescript": "^3.7.4",
"typescript-formatter": "^7.2.2" "typescript-formatter": "^7.2.2"
} }
} }

17
release.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/bash
# actions requires a node_modules dir https://github.com/actions/toolkit/blob/master/docs/javascript-action.md#publish-a-releasesv1-action
# but its recommended not to check these in https://github.com/actions/toolkit/blob/master/docs/action-versioning.md#recommendations
# as such the following hack is how we dill with it
if [[ $# -ne 1 ]]; then
echo "please pass a release version. i.e. $0 v1"
exit 1
fi
git checkout -b releases/$1 # If this branch already exists, omit the -b flag
rm -rf node_modules
sed -i '/node_modules/d' .gitignore # Bash command that removes node_modules from .gitignore
npm install --production
git add node_modules -f .gitignore
git commit -m node_modules
git push origin releases/$1

View File

@ -1,7 +1,7 @@
import fetch from "node-fetch";
import { GitHub } from "@actions/github/lib/utils"; import { GitHub } from "@actions/github/lib/utils";
import { Config, isTag, releaseBody, alignAssetName } from "./util"; import { Config, isTag, releaseBody } from "./util";
import { statSync } from "fs"; import { lstatSync, readFileSync } from "fs";
import { open } from "fs/promises";
import { getType } from "mime"; import { getType } from "mime";
import { basename } from "path"; import { basename } from "path";
@ -11,6 +11,7 @@ export interface ReleaseAsset {
name: string; name: string;
mime: string; mime: string;
size: number; size: number;
data: Buffer;
} }
export interface Release { export interface Release {
@ -43,8 +44,6 @@ export interface Releaser {
prerelease: boolean | undefined; prerelease: boolean | undefined;
target_commitish: string | undefined; target_commitish: string | undefined;
discussion_category_name: string | undefined; discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
make_latest: "true" | "false" | "legacy" | undefined;
}): Promise<{ data: Release }>; }): Promise<{ data: Release }>;
updateRelease(params: { updateRelease(params: {
@ -58,8 +57,6 @@ export interface Releaser {
draft: boolean | undefined; draft: boolean | undefined;
prerelease: boolean | undefined; prerelease: boolean | undefined;
discussion_category_name: string | undefined; discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
make_latest: "true" | "false" | "legacy" | undefined;
}): Promise<{ data: Release }>; }): Promise<{ data: Release }>;
allReleases(params: { allReleases(params: {
@ -92,16 +89,7 @@ export class GitHubReleaser implements Releaser {
prerelease: boolean | undefined; prerelease: boolean | undefined;
target_commitish: string | undefined; target_commitish: string | undefined;
discussion_category_name: string | undefined; discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
make_latest: "true" | "false" | "legacy" | undefined;
}): Promise<{ data: Release }> { }): Promise<{ data: Release }> {
if (
typeof params.make_latest === "string" &&
!["true", "false", "legacy"].includes(params.make_latest)
) {
params.make_latest = undefined;
}
return this.github.rest.repos.createRelease(params); return this.github.rest.repos.createRelease(params);
} }
@ -116,16 +104,7 @@ export class GitHubReleaser implements Releaser {
draft: boolean | undefined; draft: boolean | undefined;
prerelease: boolean | undefined; prerelease: boolean | undefined;
discussion_category_name: string | undefined; discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
make_latest: "true" | "false" | "legacy" | undefined;
}): Promise<{ data: Release }> { }): Promise<{ data: Release }> {
if (
typeof params.make_latest === "string" &&
!["true", "false", "legacy"].includes(params.make_latest)
) {
params.make_latest = undefined;
}
return this.github.rest.repos.updateRelease(params); return this.github.rest.repos.updateRelease(params);
} }
@ -135,7 +114,7 @@ export class GitHubReleaser implements Releaser {
}): AsyncIterableIterator<{ data: Release[] }> { }): AsyncIterableIterator<{ data: Release[] }> {
const updatedParams = { per_page: 100, ...params }; const updatedParams = { per_page: 100, ...params };
return this.github.paginate.iterator( return this.github.paginate.iterator(
this.github.rest.repos.listReleases.endpoint.merge(updatedParams), this.github.rest.repos.listReleases.endpoint.merge(updatedParams)
); );
} }
} }
@ -144,7 +123,8 @@ export const asset = (path: string): ReleaseAsset => {
return { return {
name: basename(path), name: basename(path),
mime: mimeOrDefault(path), mime: mimeOrDefault(path),
size: statSync(path).size, size: lstatSync(path).size,
data: readFileSync(path)
}; };
}; };
@ -157,58 +137,48 @@ export const upload = async (
github: GitHub, github: GitHub,
url: string, url: string,
path: string, path: string,
currentAssets: Array<{ id: number; name: string }>, currentAssets: Array<{ id: number; name: string }>
): Promise<any> => { ): Promise<any> => {
const [owner, repo] = config.github_repository.split("/"); const [owner, repo] = config.github_repository.split("/");
const { name, mime, size } = asset(path); const { name, size, mime, data: body } = asset(path);
const currentAsset = currentAssets.find( const currentAsset = currentAssets.find(
// note: GitHub renames asset filenames that have special characters, non-alphanumeric characters, and leading or trailing periods. The "List release assets" endpoint lists the renamed filenames. ({ name: currentName }) => currentName == name
// due to this renaming we need to be mindful when we compare the file name we're uploading with a name github may already have rewritten for logical comparison
// see https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset
({ name: currentName }) => currentName == alignAssetName(name),
); );
if (currentAsset) { if (currentAsset) {
console.log(`♻️ Deleting previously uploaded asset ${name}...`); console.log(`♻️ Deleting previously uploaded asset ${name}...`);
await github.rest.repos.deleteReleaseAsset({ await github.rest.repos.deleteReleaseAsset({
asset_id: currentAsset.id || 1, asset_id: currentAsset.id || 1,
owner, owner,
repo, repo
}); });
} }
console.log(`⬆️ Uploading ${name}...`); console.log(`⬆️ Uploading ${name}...`);
const endpoint = new URL(url); const endpoint = new URL(url);
endpoint.searchParams.append("name", name); endpoint.searchParams.append("name", name);
const fh = await open(path); const resp = await fetch(endpoint, {
try {
const resp = await github.request({
method: "POST",
url: endpoint.toString(),
headers: { headers: {
"content-length": `${size}`, "content-length": `${size}`,
"content-type": mime, "content-type": mime,
authorization: `token ${config.github_token}`, authorization: `token ${config.github_token}`
}, },
data: fh.readableWebStream({ type: "bytes" }), method: "POST",
body
}); });
const json = resp.data; const json = await resp.json();
if (resp.status !== 201) { if (resp.status !== 201) {
throw new Error( throw new Error(
`Failed to upload release asset ${name}. received status code ${ `Failed to upload release asset ${name}. recieved status code ${
resp.status resp.status
}\n${json.message}\n${JSON.stringify(json.errors)}`, }\n${json.message}\n${JSON.stringify(json.errors)}`
); );
} }
console.log(`✅ Uploaded ${name}`);
return json; return json;
} finally {
await fh.close();
}
}; };
export const release = async ( export const release = async (
config: Config, config: Config,
releaser: Releaser, releaser: Releaser,
maxRetries: number = 3, maxRetries: number = 3
): Promise<Release> => { ): Promise<Release> => {
if (maxRetries <= 0) { if (maxRetries <= 0) {
console.log(`❌ Too many retries. Aborting...`); console.log(`❌ Too many retries. Aborting...`);
@ -223,77 +193,57 @@ export const release = async (
: ""); : "");
const discussion_category_name = config.input_discussion_category_name; const discussion_category_name = config.input_discussion_category_name;
const generate_release_notes = config.input_generate_release_notes;
try { try {
// you can't get an existing draft by tag // you can't get a an existing draft by tag
// so we must find one in the list of all releases // so we must find one in the list of all releases
let _release: Release | undefined = undefined; if (config.input_draft) {
for await (const response of releaser.allReleases({ for await (const response of releaser.allReleases({
owner, owner,
repo, repo
})) { })) {
_release = response.data.find((release) => release.tag_name === tag); let release = response.data.find(release => release.tag_name === tag);
if (_release !== undefined) { if (release) {
break; return release;
} }
} }
if (_release === undefined) { }
return await createRelease( let existingRelease = await releaser.getReleaseByTag({
tag,
config,
releaser,
owner, owner,
repo, repo,
discussion_category_name, tag
generate_release_notes, });
maxRetries,
);
}
let existingRelease: Release = _release!; const release_id = existingRelease.data.id;
console.log(
`Found release ${existingRelease.name} (with id=${existingRelease.id})`,
);
const release_id = existingRelease.id;
let target_commitish: string; let target_commitish: string;
if ( if (
config.input_target_commitish && config.input_target_commitish &&
config.input_target_commitish !== existingRelease.target_commitish config.input_target_commitish !== existingRelease.data.target_commitish
) { ) {
console.log( console.log(
`Updating commit from "${existingRelease.target_commitish}" to "${config.input_target_commitish}"`, `Updating commit from "${existingRelease.data.target_commitish}" to "${config.input_target_commitish}"`
); );
target_commitish = config.input_target_commitish; target_commitish = config.input_target_commitish;
} else { } else {
target_commitish = existingRelease.target_commitish; target_commitish = existingRelease.data.target_commitish;
} }
const tag_name = tag; const tag_name = tag;
const name = config.input_name || existingRelease.name || tag; const name = config.input_name || existingRelease.data.name || tag;
// revisit: support a new body-concat-strategy input for accumulating
// body parts as a release gets updated. some users will likely want this while let body: string = "";
// others won't previously this was duplicating content for most which if (existingRelease.data.body) body += existingRelease.data.body;
// no one wants let workflowBody = releaseBody(config);
const workflowBody = releaseBody(config) || ""; if (existingRelease.data.body && workflowBody) body += "\n";
const existingReleaseBody = existingRelease.body || ""; if (workflowBody) body += workflowBody;
let body: string;
if (config.input_append_body && workflowBody && existingReleaseBody) {
body = existingReleaseBody + "\n" + workflowBody;
} else {
body = workflowBody || existingReleaseBody;
}
const draft = const draft =
config.input_draft !== undefined config.input_draft !== undefined
? config.input_draft ? config.input_draft
: existingRelease.draft; : existingRelease.data.draft;
const prerelease = const prerelease =
config.input_prerelease !== undefined config.input_prerelease !== undefined
? config.input_prerelease ? config.input_prerelease
: existingRelease.prerelease; : existingRelease.data.prerelease;
const make_latest = config.input_make_latest;
const release = await releaser.updateRelease({ const release = await releaser.updateRelease({
owner, owner,
@ -305,55 +255,23 @@ export const release = async (
body, body,
draft, draft,
prerelease, prerelease,
discussion_category_name, discussion_category_name
generate_release_notes,
make_latest,
}); });
return release.data; return release.data;
} catch (error) { } catch (error) {
if (error.status !== 404) { if (error.status === 404) {
console.log(
`⚠️ Unexpected error fetching GitHub release for tag ${config.github_ref}: ${error}`,
);
throw error;
}
return await createRelease(
tag,
config,
releaser,
owner,
repo,
discussion_category_name,
generate_release_notes,
maxRetries,
);
}
};
async function createRelease(
tag: string,
config: Config,
releaser: Releaser,
owner: string,
repo: string,
discussion_category_name: string | undefined,
generate_release_notes: boolean | undefined,
maxRetries: number,
) {
const tag_name = tag; const tag_name = tag;
const name = config.input_name || tag; const name = config.input_name || tag;
const body = releaseBody(config); const body = releaseBody(config);
const draft = config.input_draft; const draft = config.input_draft;
const prerelease = config.input_prerelease; const prerelease = config.input_prerelease;
const target_commitish = config.input_target_commitish; const target_commitish = config.input_target_commitish;
const make_latest = config.input_make_latest;
let commitMessage: string = ""; let commitMessage: string = "";
if (target_commitish) { if (target_commitish) {
commitMessage = ` using commit "${target_commitish}"`; commitMessage = ` using commit "${target_commitish}"`;
} }
console.log( console.log(
`👩‍🏭 Creating new GitHub release for tag ${tag_name}${commitMessage}...`, `👩‍🏭 Creating new GitHub release for tag ${tag_name}${commitMessage}...`
); );
try { try {
let release = await releaser.createRelease({ let release = await releaser.createRelease({
@ -365,33 +283,23 @@ async function createRelease(
draft, draft,
prerelease, prerelease,
target_commitish, target_commitish,
discussion_category_name, discussion_category_name
generate_release_notes,
make_latest,
}); });
return release.data; return release.data;
} catch (error) { } catch (error) {
// presume a race with competing matrix runs // presume a race with competing metrix runs
console.log(`⚠️ GitHub release failed with status: ${error.status}`);
console.log(`${JSON.stringify(error.response.data)}`);
switch (error.status) {
case 403:
console.log( console.log(
"Skip retry — your GitHub token/PAT does not have the required permission to create a release", `⚠️ GitHub release failed with status: ${
error.status
}, retrying... (${maxRetries - 1} retries remaining)`
); );
throw error;
case 404:
console.log("Skip retry - discussion category mismatch");
throw error;
case 422:
console.log("Skip retry - validation failed");
throw error;
}
console.log(`retrying... (${maxRetries - 1} retries remaining)`);
return release(config, releaser, maxRetries - 1); return release(config, releaser, maxRetries - 1);
} }
} } else {
console.log(
`⚠️ Unexpected error fetching GitHub release for tag ${config.github_ref}: ${error}`
);
throw error;
}
}
};

View File

@ -3,11 +3,12 @@ import {
parseConfig, parseConfig,
isTag, isTag,
unmatchedPatterns, unmatchedPatterns,
uploadUrl, uploadUrl
} from "./util"; } from "./util";
import { release, upload, GitHubReleaser } from "./github"; import { release, upload, GitHubReleaser } from "./github";
import { getOctokit } from "@actions/github"; import { getOctokit } from "@actions/github";
import { setFailed, setOutput } from "@actions/core"; import { setFailed, setOutput } from "@actions/core";
import { GitHub, getOctokitOptions } from "@actions/github/lib/utils";
import { env } from "process"; import { env } from "process";
@ -23,13 +24,9 @@ async function run() {
} }
if (config.input_files) { if (config.input_files) {
const patterns = unmatchedPatterns(config.input_files); const patterns = unmatchedPatterns(config.input_files);
patterns.forEach((pattern) => { patterns.forEach(pattern =>
if (config.input_fail_on_unmatched_files) { console.warn(`🤔 Pattern '${pattern}' does not match any files.`)
throw new Error(`⚠️ Pattern '${pattern}' does not match any files.`); );
} else {
console.warn(`🤔 Pattern '${pattern}' does not match any files.`);
}
});
if (patterns.length > 0 && config.input_fail_on_unmatched_files) { if (patterns.length > 0 && config.input_fail_on_unmatched_files) {
throw new Error(`⚠️ There were unmatched files`); throw new Error(`⚠️ There were unmatched files`);
} }
@ -45,7 +42,7 @@ async function run() {
throttle: { throttle: {
onRateLimit: (retryAfter, options) => { onRateLimit: (retryAfter, options) => {
console.warn( console.warn(
`Request quota exhausted for request ${options.method} ${options.url}`, `Request quota exhausted for request ${options.method} ${options.url}`
); );
if (options.request.retryCount === 0) { if (options.request.retryCount === 0) {
// only retries once // only retries once
@ -56,50 +53,32 @@ async function run() {
onAbuseLimit: (retryAfter, options) => { onAbuseLimit: (retryAfter, options) => {
// does not retry, only logs a warning // does not retry, only logs a warning
console.warn( console.warn(
`Abuse detected for request ${options.method} ${options.url}`, `Abuse detected for request ${options.method} ${options.url}`
); );
}, }
}, }
}); });
//); //);
const rel = await release(config, new GitHubReleaser(gh)); const rel = await release(config, new GitHubReleaser(gh));
if (config.input_files && config.input_files.length > 0) { if (config.input_files) {
const files = paths(config.input_files); const files = paths(config.input_files);
if (files.length == 0) { if (files.length == 0) {
if (config.input_fail_on_unmatched_files) { console.warn(`🤔 ${config.input_files} not include valid file.`);
throw new Error(
`⚠️ ${config.input_files} does not include a valid file.`,
);
} else {
console.warn(
`🤔 ${config.input_files} does not include a valid file.`,
);
} }
} const currentAsserts = rel.assets;
const currentAssets = rel.assets; await Promise.all(
files.map(async path => {
const uploadFile = async (path) => { await upload(
const json = await upload(
config, config,
gh, gh,
uploadUrl(rel.upload_url), uploadUrl(rel.upload_url),
path, path,
currentAssets, currentAsserts
); );
delete json.uploader; })
return json; ).catch(error => {
}; throw error;
});
let assets;
if (!config.input_preserve_order) {
assets = await Promise.all(files.map(uploadFile));
} else {
assets = [];
for (const path of files) {
assets.push(await uploadFile(path));
}
}
setOutput("assets", assets);
} }
console.log(`🎉 Release ready at ${rel.html_url}`); console.log(`🎉 Release ready at ${rel.html_url}`);
setOutput("url", rel.html_url); setOutput("url", rel.html_url);

View File

@ -1,5 +1,5 @@
import * as glob from "glob"; import * as glob from "glob";
import { statSync, readFileSync } from "fs"; import { lstatSync, readFileSync } from "fs";
export interface Config { export interface Config {
github_token: string; github_token: string;
@ -13,14 +13,10 @@ export interface Config {
input_body_path?: string; input_body_path?: string;
input_files?: string[]; input_files?: string[];
input_draft?: boolean; input_draft?: boolean;
input_preserve_order?: boolean;
input_prerelease?: boolean; input_prerelease?: boolean;
input_fail_on_unmatched_files?: boolean; input_fail_on_unmatched_files?: boolean;
input_target_commitish?: string; input_target_commitish?: string;
input_discussion_category_name?: string; input_discussion_category_name?: string;
input_generate_release_notes?: boolean;
input_append_body?: boolean;
input_make_latest: "true" | "false" | "legacy" | undefined;
} }
export const uploadUrl = (url: string): string => { export const uploadUrl = (url: string): string => {
@ -46,9 +42,9 @@ export const parseInputFiles = (files: string): string[] => {
(acc, line) => (acc, line) =>
acc acc
.concat(line.split(",")) .concat(line.split(","))
.filter((pat) => pat) .filter(pat => pat)
.map((pat) => pat.trim()), .map(pat => pat.trim()),
[], []
); );
}; };
@ -63,35 +59,19 @@ export const parseConfig = (env: Env): Config => {
input_body_path: env.INPUT_BODY_PATH, input_body_path: env.INPUT_BODY_PATH,
input_files: parseInputFiles(env.INPUT_FILES || ""), input_files: parseInputFiles(env.INPUT_FILES || ""),
input_draft: env.INPUT_DRAFT ? env.INPUT_DRAFT === "true" : undefined, input_draft: env.INPUT_DRAFT ? env.INPUT_DRAFT === "true" : undefined,
input_preserve_order: env.INPUT_PRESERVE_ORDER
? env.INPUT_PRESERVE_ORDER == "true"
: undefined,
input_prerelease: env.INPUT_PRERELEASE input_prerelease: env.INPUT_PRERELEASE
? env.INPUT_PRERELEASE == "true" ? env.INPUT_PRERELEASE == "true"
: undefined, : undefined,
input_fail_on_unmatched_files: env.INPUT_FAIL_ON_UNMATCHED_FILES == "true", input_fail_on_unmatched_files: env.INPUT_FAIL_ON_UNMATCHED_FILES == "true",
input_target_commitish: env.INPUT_TARGET_COMMITISH || undefined, input_target_commitish: env.INPUT_TARGET_COMMITISH,
input_discussion_category_name: input_discussion_category_name: env.INPUT_DISCUSSION_CATEGORY_NAME
env.INPUT_DISCUSSION_CATEGORY_NAME || undefined,
input_generate_release_notes: env.INPUT_GENERATE_RELEASE_NOTES == "true",
input_append_body: env.INPUT_APPEND_BODY == "true",
input_make_latest: parseMakeLatest(env.INPUT_MAKE_LATEST),
}; };
}; };
const parseMakeLatest = (
value: string | undefined,
): "true" | "false" | "legacy" | undefined => {
if (value === "true" || value === "false" || value === "legacy") {
return value;
}
return undefined;
};
export const paths = (patterns: string[]): string[] => { export const paths = (patterns: string[]): string[] => {
return patterns.reduce((acc: string[], pattern: string): string[] => { return patterns.reduce((acc: string[], pattern: string): string[] => {
return acc.concat( return acc.concat(
glob.sync(pattern).filter((path) => statSync(path).isFile()), glob.sync(pattern).filter(path => lstatSync(path).isFile())
); );
}, []); }, []);
}; };
@ -99,9 +79,9 @@ export const paths = (patterns: string[]): string[] => {
export const unmatchedPatterns = (patterns: string[]): string[] => { export const unmatchedPatterns = (patterns: string[]): string[] => {
return patterns.reduce((acc: string[], pattern: string): string[] => { return patterns.reduce((acc: string[], pattern: string): string[] => {
return acc.concat( return acc.concat(
glob.sync(pattern).filter((path) => statSync(path).isFile()).length == 0 glob.sync(pattern).filter(path => lstatSync(path).isFile()).length == 0
? [pattern] ? [pattern]
: [], : []
); );
}, []); }, []);
}; };
@ -109,7 +89,3 @@ export const unmatchedPatterns = (patterns: string[]): string[] => {
export const isTag = (ref: string): boolean => { export const isTag = (ref: string): boolean => {
return ref.startsWith("refs/tags/"); return ref.startsWith("refs/tags/");
}; };
export const alignAssetName = (assetName: string): string => {
return assetName.replace(/ /g, ".");
};

View File

@ -1,6 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"useUnknownInCatchVariables": false,
/* Basic Options */ /* Basic Options */
// "incremental": true, /* Enable incremental compilation */ // "incremental": true, /* Enable incremental compilation */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
@ -60,5 +59,5 @@
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}, },
"exclude": ["node_modules", "**/*.test.ts", "jest.config.ts"] "exclude": ["node_modules", "**/*.test.ts"]
} }