Compare commits

..

6 Commits

Author SHA1 Message Date
e6a400341a check style a different way 2020-01-05 17:41:51 -05:00
5733db0089 update deps 2020-01-05 17:32:34 -05:00
7060560593 update dist 2020-01-05 17:23:13 -05:00
7b2fd2c223 newer checkout 2020-01-05 17:05:34 -05:00
e7bba17971 consistent 2020-01-05 17:05:16 -05:00
03caddd29a try ncc for packaging 2020-01-05 17:03:58 -05:00
18 changed files with 4270 additions and 7637 deletions

View File

@ -1,14 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
ignore:
- dependency-name: node-fetch
versions:
- ">=3.0.0"
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly

View File

@ -8,7 +8,7 @@ jobs:
steps: steps:
# https://github.com/actions/checkout # https://github.com/actions/checkout
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Install - name: Install
run: npm ci run: npm ci
- name: Build - name: Build

1
.nvmrc
View File

@ -1 +0,0 @@
20.11.1

View File

@ -1,108 +1,43 @@
## 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 declartion 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
- fixed error message formatting for file uploads
## 0.1.9
- add support for linking release to GitHub discussion [#136](https://github.com/softprops/action-gh-release/pull/136)
## 0.1.8
- address recent warnings in assert upload api as well as introduce asset upload overrides, allowing for multiple runs for the same release with the same named asserts [#134](https://github.com/softprops/action-gh-release/pull/134)
- fix backwards compatibility with `GITHUB_TOKEN` resolution. `GITHUB_TOKEN` is no resolved first from an env varibale and then from and input [#133](https://github.com/softprops/action-gh-release/pull/133)
- trim white space in provided `tag_name` [#130](https://github.com/softprops/action-gh-release/pull/130)
## 0.1.7
- allow creating draft releases without a tag [#95](https://github.com/softprops/action-gh-release/pull/95)
- Set default token for simpler setup [#83](https://github.com/softprops/action-gh-release/pull/83)
- fix regression with action yml [#126](https://github.com/softprops/action-gh-release/pull/126)
## 0.1.6
This is a release catch up have a hiatus. Future releases will happen more frequently
- Add 'fail_on_unmatched_files' input, useful for catching cases were your `files` input does not actually match what you expect [#55](https://github.com/softprops/action-gh-release/pull/55)
- Add `repository` input, useful for creating a release in an external repository [#61](https://github.com/softprops/action-gh-release/pull/61)
- Add release `id` to outputs, useful for refering to release in workflow steps following the step that uses this action [#60](https://github.com/softprops/action-gh-release/pull/60)
- Add `upload_url` as action output, useful for managing uploads separately [#75](https://github.com/softprops/action-gh-release/pull/75)
- Support custom `target_commitish` value, useful to customize the default [#76](https://github.com/softprops/action-gh-release/pull/76)
- fix `body_path` input first then fall back on `body` input. this was the originally documented precedence but was implemened the the opposite order! [#85](https://github.com/softprops/action-gh-release/pull/85)
- Retain original release info if the keys are not set, useful for filling in blanks for a release you've already started separately [#109](https://github.com/softprops/action-gh-release/pull/109)
- Limit number of times github api request to create a release is retried, useful for avoiding eating up your rate limit and action minutes do to either an invalid token or other circumstance causing the api call to fail [#111](https://github.com/softprops/action-gh-release/pull/111)
## 0.1.5
- Added support for specifying tag name [#39](https://github.com/softprops/action-gh-release/pull/39)
## 0.1.4 ## 0.1.4
- Added support for updating releases body [#36](https://github.com/softprops/action-gh-release/pull/36) * Steps can now access the url of releases with the `url` output of this Action [#28](https://github.com/softprops/action-gh-release/pull/28)
- Steps can now access the url of releases with the `url` output of this Action [#28](https://github.com/softprops/action-gh-release/pull/28) * Added basic GitHub API retry support to manage API turbulance [#26](https://github.com/softprops/action-gh-release/pull/26)
- Added basic GitHub API retry support to manage API turbulance [#26](https://github.com/softprops/action-gh-release/pull/26)
## 0.1.3 ## 0.1.3
- Fixed where `with: body_path` was not being used in generated GitHub releases * Fixed where `with: body_path` was not being used in generated GitHub releases
## 0.1.2 ## 0.1.2
- Add support for merging draft releases [#16](https://github.com/softprops/action-gh-release/pull/16) * Add support for merging draft releases [#16](https://github.com/softprops/action-gh-release/pull/16)
GitHub's api doesn't explicitly have a way of fetching a draft release by tag name which caused draft releases to appear as separate releases when used in a build matrix. GitHub's api doesn't explicitly have a way of fetching a draft release by tag name which caused draft releases to appear as separate releases when used in a build matrix.
This is now fixed. This is now fixed.
- Add support for newline-delimited asset list [#18](https://github.com/softprops/action-gh-release/pull/18) * Add support for newline-delimited asset list [#18](https://github.com/softprops/action-gh-release/pull/18)
GitHub actions inputs don't inherently support lists of things and one might like to append a list of files to include in a release. Previously this was possible using a comma-delimited list of asset path patterns to upload. You can now provide these as a newline delimieted list for better readability GitHub actions inputs don't inherently support lists of things and one might like to append a list of files to include in a release. Previously this was possible using a comma-delimited list of asset path patterns to upload. You can now provide these as a newline delimieted list for better readability
```yaml ```yaml
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
files: | files: |
filea.txt filea.txt
fileb.txt fileb.txt
filec.txt filec.txt
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
``` ```
- Add support for prerelease annotated GitHub releases with the new input field `with.prerelease: true` [#19](https://github.com/softprops/action-gh-release/pull/19) * Add support for prerelease annotated GitHub releases with the new input field `with.prerelease: true` [#19](https://github.com/softprops/action-gh-release/pull/19)
--- ---
## 0.1.1 ## 0.1.1
- Add support for publishing releases on all supported virtual hosts * Add support for publishing releases on all supported virtual hosts
You'll need to remove `docker://` prefix and use the `@v1` action tag You'll need to remove `docker://` prefix and use the `@v1` action tag
@ -110,4 +45,4 @@ You'll need to remove `docker://` prefix and use the `@v1` action tag
## 0.1.0 ## 0.1.0
- Initial release * Initial release

View File

@ -1,12 +1,12 @@
## bootstrapping ## bootstrapping
This a [JavaScript](https://help.github.com/en/articles/about-actions#types-of-actions) action but uses [TypeScript](https://www.typescriptlang.org/docs/home.html) to generate that JavaScript. This a [JavaScript](https://help.github.com/en/articles/about-actions#types-of-actions) action but uses [TypeScript](https://www.typescriptlang.org/docs/home.html) to generate that JavaScript.
You can bootstrap your environment with a modern version of npm and by running `npm i` at the root of this repo. You can bootstrap your envrinment with a modern version of npm and by running `npm i` at the root of this repo.
## testing ## testing
Tests can be found under under `__tests__` directory and are runnable with the `npm t` command. Tests can be found under under `__tests__` directory and are runnable with the `npm t` command
## source code ## source code
@ -14,4 +14,5 @@ Source code can be found under the `src` directory. Running `npm run build` will
## formatting ## formatting
A minimal attempt at keeping a consistent code style is can be applied by running `npm run fmt`. A minimal attempt at keeping a consistent code style is can be applied by running `npm run fmt`

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

121
README.md
View File

@ -1,3 +1,4 @@
<div align="center"> <div align="center">
📦 :octocat: 📦 :octocat:
</div> </div>
@ -6,7 +7,7 @@
</h1> </h1>
<p align="center"> <p align="center">
A GitHub Action for creating GitHub Releases on Linux, Windows, and macOS virtual environments A GitHub Action for creating GitHub Releases on Linux, Windows, and OSX virtual environments
</p> </p>
<div align="center"> <div align="center">
@ -19,6 +20,7 @@
</a> </a>
</div> </div>
<br /> <br />
## 🤸 Usage ## 🤸 Usage
@ -27,7 +29,7 @@
Typically usage of this action involves adding a step to a build that Typically usage of this action involves adding a step to a build that
is gated pushes to git tags. You may find `step.if` field helpful in accomplishing this is gated pushes to git tags. You may find `step.if` field helpful in accomplishing this
as it maximizes the reuse value of your workflow for non-tag pushes. as it maximizes the resuse value of your workflow for non-tag pushes.
Below is a simple example of `step.if` tag gating Below is a simple example of `step.if` tag gating
@ -41,10 +43,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v1
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
``` ```
You can also use push config tag filter You can also use push config tag filter
@ -55,21 +59,23 @@ name: Main
on: on:
push: push:
tags: tags:
- "v*.*.*" - 'v*.*.*'
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v1
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
``` ```
### ⬆️ Uploading release assets ### ⬆️ Uploading release assets
You can configure a number of options for your You can can configure a number of options for your
GitHub release and all are optional. 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.
@ -88,16 +94,18 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v1
- 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: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
files: Release.txt files: Release.txt
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
``` ```
Below is an example of uploading more than one asset with a GitHub release Below is an example of uploading more than one asset with a GitHub release
@ -112,24 +120,24 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v1
- 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: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
files: | files: |
Release.txt Release.txt
LICENSE LICENSE
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
``` ```
> **⚠️ 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:** 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
Many systems exist that can help generate release notes for you. This action supports Many systems exist that can help generate release notes for you. This action supports
@ -146,19 +154,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v1
- name: Generate Changelog - name: Generate Changelog
run: echo "# Good things have arrived" > ${{ github.workspace }}-CHANGELOG.txt run: echo "# Good things have arrived" > ${{ github.workflow }}-CHANGELOG.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
body_path: ${{ github.workspace }}-CHANGELOG.txt body_path: ${{ github.workflow }}-CHANGELOG.txt
# note you'll typically need to create a personal access token
# with permissions to create releases in the other repo
token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
env: env:
GITHUB_REPOSITORY: my_gh_org/my_gh_repo GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
``` ```
### 💅 Customizing ### 💅 Customizing
@ -167,71 +172,37 @@ 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 |
| `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` |
| `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 |
| `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. |
| `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) |
| `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 |
💡 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.
💡 When the release info keys (such as `name`, `body`, `draft`, `prerelease`, etc.)
are not explicitly set and there is already an existing release for the tag, the
release will retain its original info.
#### outputs #### outputs
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 |
| `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
The following `step.env` keys are allowed as a fallback but deprecated in favor of using inputs. The following are *required* as `step.env` keys
| Name | Description | | Name | Description |
| ------------------- | ------------------------------------------------------------------------------------------ | |----------------|--------------------------------------|
| `GITHUB_TOKEN` | GITHUB_TOKEN as provided by `secrets` | | `GITHUB_TOKEN` | GITHUB_TOKEN as provided by `secrets`|
| `GITHUB_REPOSITORY` | Name of a target repository in `<owner>/<repo>` format. defaults to the current repository |
> **⚠️ 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 > **⚠️ 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
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.
Doug Tangren (softprops) 2019 Doug Tangren (softprops) 2019

View File

@ -15,11 +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, data } = asset("tests/data/foo/bar.txt"); const { name, mime, size, file } = 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"); assert.equal(file.toString(), "release me");
}); });
}); });
}); });

View File

@ -3,23 +3,11 @@ import {
isTag, isTag,
paths, paths,
parseConfig, parseConfig,
parseInputFiles, parseInputFiles
unmatchedPatterns,
uploadUrl,
} from "../src/util"; } from "../src/util";
import * as assert from "assert"; import * as assert from "assert";
describe("util", () => { describe("util", () => {
describe("uploadUrl", () => {
it("strips template", () => {
assert.equal(
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"
);
});
});
describe("parseInputFiles", () => { describe("parseInputFiles", () => {
it("parses empty strings", () => { it("parses empty strings", () => {
assert.deepStrictEqual(parseInputFiles(""), []); assert.deepStrictEqual(parseInputFiles(""), []);
@ -47,11 +35,7 @@ describe("util", () => {
input_draft: false, input_draft: false,
input_prerelease: false, input_prerelease: false,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined
input_tag_name: undefined,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
}) })
); );
}); });
@ -67,17 +51,13 @@ describe("util", () => {
input_draft: false, input_draft: false,
input_prerelease: false, input_prerelease: false,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined
input_tag_name: undefined,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
}) })
); );
}); });
it("defaults to body path when both body and body path are provided", () => { it("defaults to body when both body and body path are provided", () => {
assert.equal( assert.equal(
"bar", "foo",
releaseBody({ releaseBody({
github_ref: "", github_ref: "",
github_repository: "", github_repository: "",
@ -87,223 +67,24 @@ describe("util", () => {
input_draft: false, input_draft: false,
input_prerelease: false, input_prerelease: false,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined
input_tag_name: undefined,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
}) })
); );
}); });
}); });
describe("parseConfig", () => { describe("parseConfig", () => {
it("parses basic config", () => { it("parses basic config", () => {
assert.deepStrictEqual( assert.deepStrictEqual(parseConfig({}), {
parseConfig({ github_ref: "",
// note: inputs declared in actions.yml, even when declared not required, github_repository: "",
// are still provided by the actions runtime env as empty strings instead of github_token: "",
// the normal absent env value one would expect. this breaks things input_body: undefined,
// as an empty string !== undefined in terms of what we pass to the api input_body_path: undefined,
// so we cover that in a test case here to ensure undefined values are actually input_draft: false,
// resolved as undefined and not empty strings input_prerelease: false,
INPUT_TARGET_COMMITISH: "", input_files: [],
INPUT_DISCUSSION_CATEGORY_NAME: "", input_name: undefined
}), });
{
github_ref: "",
github_repository: "",
github_token: "",
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
input_draft: undefined,
input_prerelease: 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,
}
);
});
it("parses basic config with commitish", () => {
assert.deepStrictEqual(
parseConfig({
INPUT_TARGET_COMMITISH: "affa18ef97bc9db20076945705aba8c516139abd",
}),
{
github_ref: "",
github_repository: "",
github_token: "",
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
input_draft: undefined,
input_prerelease: undefined,
input_files: [],
input_name: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
input_target_commitish: "affa18ef97bc9db20076945705aba8c516139abd",
input_discussion_category_name: undefined,
input_generate_release_notes: false,
}
);
});
it("supports discussion category names", () => {
assert.deepStrictEqual(
parseConfig({
INPUT_DISCUSSION_CATEGORY_NAME: "releases",
}),
{
github_ref: "",
github_repository: "",
github_token: "",
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
input_draft: undefined,
input_prerelease: undefined,
input_files: [],
input_name: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
input_target_commitish: undefined,
input_discussion_category_name: "releases",
input_generate_release_notes: false,
}
);
});
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_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,
}
);
});
it("prefers GITHUB_TOKEN over token input for backwards compatibility", () => {
assert.deepStrictEqual(
parseConfig({
INPUT_DRAFT: "false",
INPUT_PRERELEASE: "true",
GITHUB_TOKEN: "env-token",
INPUT_TOKEN: "input-token",
}),
{
github_ref: "",
github_repository: "",
github_token: "env-token",
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
input_draft: false,
input_prerelease: true,
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,
}
);
});
it("uses input token as the source of GITHUB_TOKEN by default", () => {
assert.deepStrictEqual(
parseConfig({
INPUT_DRAFT: "false",
INPUT_PRERELEASE: "true",
INPUT_TOKEN: "input-token",
}),
{
github_ref: "",
github_repository: "",
github_token: "input-token",
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
input_draft: false,
input_prerelease: true,
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,
}
);
});
it("parses basic config with draft and prerelease", () => {
assert.deepStrictEqual(
parseConfig({
INPUT_DRAFT: "false",
INPUT_PRERELEASE: "true",
}),
{
github_ref: "",
github_repository: "",
github_token: "",
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
input_draft: false,
input_prerelease: true,
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,
}
);
});
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_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,
}
);
}); });
}); });
describe("isTag", () => { describe("isTag", () => {
@ -317,19 +98,9 @@ describe("util", () => {
describe("paths", () => { describe("paths", () => {
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/**/*"]), [
paths(["tests/data/**/*", "tests/data/does/not/exist/*"]), "tests/data/foo/bar.txt"
["tests/data/foo/bar.txt"] ]);
);
});
});
describe("unmatchedPatterns", () => {
it("returns the patterns that don't match any files", async () => {
assert.deepStrictEqual(
unmatchedPatterns(["tests/data/**/*", "tests/data/does/not/exist/*"]),
["tests/data/does/not/exist/*"]
);
}); });
}); });
}); });

View File

@ -1,65 +1,34 @@
# https://help.github.com/en/articles/metadata-syntax-for-github-actions # https://help.github.com/en/articles/metadata-syntax-for-github-actions
name: "GH Release" name: 'GH Release'
description: "Github Action for creating Github Releases" description: 'Github Action for creating Github Releases'
author: "softprops" author: 'softprops'
inputs: inputs:
body: body:
description: "Note-worthy description of changes in release" description: 'Note-worthy description of changes in release'
required: false required: false
body_path: body_path:
description: "Path to load note-worthy description of changes in release from" description: 'Path to load note-worthy description of changes in release from'
required: false required: false
name: name:
description: "Gives the release a custom name. Defaults to tag name" description: 'Gives the release a custom name. Defaults to tag name'
required: false
tag_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'
required: false required: false
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
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
fail_on_unmatched_files:
description: "Fails if any of the `files` globs match nothing. Defaults to false"
required: false
repository:
description: "Repository to make releases against, in <owner>/<repo> format"
required: false
token:
description: "Authorized secret GitHub Personal Access Token. Defaults to github.token"
required: false
default: ${{ github.token }}
target_commitish:
description: "Commitish value that determines where the Git tag is created from. Can be any branch or commit SHA."
required: false
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."
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 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'
id:
description: "Release ID"
upload_url:
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'
icon: "package" icon: 'package'

452
dist/37.index.js vendored
View File

@ -1,452 +0,0 @@
"use strict";
exports.id = 37;
exports.ids = [37];
exports.modules = {
/***/ 4037:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "toFormData": () => (/* binding */ toFormData)
/* harmony export */ });
/* harmony import */ var fetch_blob_from_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2777);
/* harmony import */ var formdata_polyfill_esm_min_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8010);
let s = 0;
const S = {
START_BOUNDARY: s++,
HEADER_FIELD_START: s++,
HEADER_FIELD: s++,
HEADER_VALUE_START: s++,
HEADER_VALUE: s++,
HEADER_VALUE_ALMOST_DONE: s++,
HEADERS_ALMOST_DONE: s++,
PART_DATA_START: s++,
PART_DATA: s++,
END: s++
};
let f = 1;
const F = {
PART_BOUNDARY: f,
LAST_BOUNDARY: f *= 2
};
const LF = 10;
const CR = 13;
const SPACE = 32;
const HYPHEN = 45;
const COLON = 58;
const A = 97;
const Z = 122;
const lower = c => c | 0x20;
const noop = () => {};
class MultipartParser {
/**
* @param {string} boundary
*/
constructor(boundary) {
this.index = 0;
this.flags = 0;
this.onHeaderEnd = noop;
this.onHeaderField = noop;
this.onHeadersEnd = noop;
this.onHeaderValue = noop;
this.onPartBegin = noop;
this.onPartData = noop;
this.onPartEnd = noop;
this.boundaryChars = {};
boundary = '\r\n--' + boundary;
const ui8a = new Uint8Array(boundary.length);
for (let i = 0; i < boundary.length; i++) {
ui8a[i] = boundary.charCodeAt(i);
this.boundaryChars[ui8a[i]] = true;
}
this.boundary = ui8a;
this.lookbehind = new Uint8Array(this.boundary.length + 8);
this.state = S.START_BOUNDARY;
}
/**
* @param {Uint8Array} data
*/
write(data) {
let i = 0;
const length_ = data.length;
let previousIndex = this.index;
let {lookbehind, boundary, boundaryChars, index, state, flags} = this;
const boundaryLength = this.boundary.length;
const boundaryEnd = boundaryLength - 1;
const bufferLength = data.length;
let c;
let cl;
const mark = name => {
this[name + 'Mark'] = i;
};
const clear = name => {
delete this[name + 'Mark'];
};
const callback = (callbackSymbol, start, end, ui8a) => {
if (start === undefined || start !== end) {
this[callbackSymbol](ui8a && ui8a.subarray(start, end));
}
};
const dataCallback = (name, clear) => {
const markSymbol = name + 'Mark';
if (!(markSymbol in this)) {
return;
}
if (clear) {
callback(name, this[markSymbol], i, data);
delete this[markSymbol];
} else {
callback(name, this[markSymbol], data.length, data);
this[markSymbol] = 0;
}
};
for (i = 0; i < length_; i++) {
c = data[i];
switch (state) {
case S.START_BOUNDARY:
if (index === boundary.length - 2) {
if (c === HYPHEN) {
flags |= F.LAST_BOUNDARY;
} else if (c !== CR) {
return;
}
index++;
break;
} else if (index - 1 === boundary.length - 2) {
if (flags & F.LAST_BOUNDARY && c === HYPHEN) {
state = S.END;
flags = 0;
} else if (!(flags & F.LAST_BOUNDARY) && c === LF) {
index = 0;
callback('onPartBegin');
state = S.HEADER_FIELD_START;
} else {
return;
}
break;
}
if (c !== boundary[index + 2]) {
index = -2;
}
if (c === boundary[index + 2]) {
index++;
}
break;
case S.HEADER_FIELD_START:
state = S.HEADER_FIELD;
mark('onHeaderField');
index = 0;
// falls through
case S.HEADER_FIELD:
if (c === CR) {
clear('onHeaderField');
state = S.HEADERS_ALMOST_DONE;
break;
}
index++;
if (c === HYPHEN) {
break;
}
if (c === COLON) {
if (index === 1) {
// empty header field
return;
}
dataCallback('onHeaderField', true);
state = S.HEADER_VALUE_START;
break;
}
cl = lower(c);
if (cl < A || cl > Z) {
return;
}
break;
case S.HEADER_VALUE_START:
if (c === SPACE) {
break;
}
mark('onHeaderValue');
state = S.HEADER_VALUE;
// falls through
case S.HEADER_VALUE:
if (c === CR) {
dataCallback('onHeaderValue', true);
callback('onHeaderEnd');
state = S.HEADER_VALUE_ALMOST_DONE;
}
break;
case S.HEADER_VALUE_ALMOST_DONE:
if (c !== LF) {
return;
}
state = S.HEADER_FIELD_START;
break;
case S.HEADERS_ALMOST_DONE:
if (c !== LF) {
return;
}
callback('onHeadersEnd');
state = S.PART_DATA_START;
break;
case S.PART_DATA_START:
state = S.PART_DATA;
mark('onPartData');
// falls through
case S.PART_DATA:
previousIndex = index;
if (index === 0) {
// boyer-moore derrived algorithm to safely skip non-boundary data
i += boundaryEnd;
while (i < bufferLength && !(data[i] in boundaryChars)) {
i += boundaryLength;
}
i -= boundaryEnd;
c = data[i];
}
if (index < boundary.length) {
if (boundary[index] === c) {
if (index === 0) {
dataCallback('onPartData', true);
}
index++;
} else {
index = 0;
}
} else if (index === boundary.length) {
index++;
if (c === CR) {
// CR = part boundary
flags |= F.PART_BOUNDARY;
} else if (c === HYPHEN) {
// HYPHEN = end boundary
flags |= F.LAST_BOUNDARY;
} else {
index = 0;
}
} else if (index - 1 === boundary.length) {
if (flags & F.PART_BOUNDARY) {
index = 0;
if (c === LF) {
// unset the PART_BOUNDARY flag
flags &= ~F.PART_BOUNDARY;
callback('onPartEnd');
callback('onPartBegin');
state = S.HEADER_FIELD_START;
break;
}
} else if (flags & F.LAST_BOUNDARY) {
if (c === HYPHEN) {
callback('onPartEnd');
state = S.END;
flags = 0;
} else {
index = 0;
}
} else {
index = 0;
}
}
if (index > 0) {
// when matching a possible boundary, keep a lookbehind reference
// in case it turns out to be a false lead
lookbehind[index - 1] = c;
} else if (previousIndex > 0) {
// if our boundary turned out to be rubbish, the captured lookbehind
// belongs to partData
const _lookbehind = new Uint8Array(lookbehind.buffer, lookbehind.byteOffset, lookbehind.byteLength);
callback('onPartData', 0, previousIndex, _lookbehind);
previousIndex = 0;
mark('onPartData');
// reconsider the current character even so it interrupted the sequence
// it could be the beginning of a new sequence
i--;
}
break;
case S.END:
break;
default:
throw new Error(`Unexpected state entered: ${state}`);
}
}
dataCallback('onHeaderField');
dataCallback('onHeaderValue');
dataCallback('onPartData');
// Update properties for the next call
this.index = index;
this.state = state;
this.flags = flags;
}
end() {
if ((this.state === S.HEADER_FIELD_START && this.index === 0) ||
(this.state === S.PART_DATA && this.index === this.boundary.length)) {
this.onPartEnd();
} else if (this.state !== S.END) {
throw new Error('MultipartParser.end(): stream ended unexpectedly');
}
}
}
function _fileName(headerValue) {
// matches either a quoted-string or a token (RFC 2616 section 19.5.1)
const m = headerValue.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i);
if (!m) {
return;
}
const match = m[2] || m[3] || '';
let filename = match.slice(match.lastIndexOf('\\') + 1);
filename = filename.replace(/%22/g, '"');
filename = filename.replace(/&#(\d{4});/g, (m, code) => {
return String.fromCharCode(code);
});
return filename;
}
async function toFormData(Body, ct) {
if (!/multipart/i.test(ct)) {
throw new TypeError('Failed to fetch');
}
const m = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
if (!m) {
throw new TypeError('no or bad content-type header, no multipart boundary');
}
const parser = new MultipartParser(m[1] || m[2]);
let headerField;
let headerValue;
let entryValue;
let entryName;
let contentType;
let filename;
const entryChunks = [];
const formData = new formdata_polyfill_esm_min_js__WEBPACK_IMPORTED_MODULE_1__/* .FormData */ .Ct();
const onPartData = ui8a => {
entryValue += decoder.decode(ui8a, {stream: true});
};
const appendToFile = ui8a => {
entryChunks.push(ui8a);
};
const appendFileToFormData = () => {
const file = new fetch_blob_from_js__WEBPACK_IMPORTED_MODULE_0__/* .File */ .$B(entryChunks, filename, {type: contentType});
formData.append(entryName, file);
};
const appendEntryToFormData = () => {
formData.append(entryName, entryValue);
};
const decoder = new TextDecoder('utf-8');
decoder.decode();
parser.onPartBegin = function () {
parser.onPartData = onPartData;
parser.onPartEnd = appendEntryToFormData;
headerField = '';
headerValue = '';
entryValue = '';
entryName = '';
contentType = '';
filename = null;
entryChunks.length = 0;
};
parser.onHeaderField = function (ui8a) {
headerField += decoder.decode(ui8a, {stream: true});
};
parser.onHeaderValue = function (ui8a) {
headerValue += decoder.decode(ui8a, {stream: true});
};
parser.onHeaderEnd = function () {
headerValue += decoder.decode();
headerField = headerField.toLowerCase();
if (headerField === 'content-disposition') {
// matches either a quoted-string or a token (RFC 2616 section 19.5.1)
const m = headerValue.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i);
if (m) {
entryName = m[2] || m[3] || '';
}
filename = _fileName(headerValue);
if (filename) {
parser.onPartData = appendToFile;
parser.onPartEnd = appendFileToFormData;
}
} else if (headerField === 'content-type') {
contentType = headerValue;
}
headerValue = '';
headerField = '';
};
for await (const chunk of Body) {
parser.write(chunk);
}
parser.end();
return formData;
}
/***/ })
};
;

8
dist/index.js vendored

File diff suppressed because one or more lines are too long

10501
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,44 +1,42 @@
{ {
"name": "action-gh-release", "name": "action-gh-release",
"version": "0.1.15", "version": "0.1.4",
"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",
"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'"
}, },
"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.10.0", "@actions/core": "^1.2.0",
"@actions/github": "^5.1.1", "@actions/github": "^2.0.0",
"@octokit/plugin-retry": "^4.0.3", "@octokit/plugin-throttling": "^2.7.1",
"@octokit/plugin-throttling": "^4.3.2", "glob": "^7.1.6",
"glob": "^8.0.3", "mime": "^2.4.4"
"mime": "^3.0.0",
"node-fetch": "^2.6.7"
}, },
"devDependencies": { "devDependencies": {
"@types/glob": "^8.0.0", "@types/glob": "^7.1.1",
"@types/jest": "^29.2.3", "@types/jest": "^24.0.25",
"@types/mime": "^3.0.1", "@types/mime": "^2.0.1",
"@types/node": "^18.11.9", "@types/node": "^12.12.24",
"@types/node-fetch": "^2.5.12", "@zeit/ncc": "^0.21.0",
"@vercel/ncc": "^0.34.0", "jest": "^24.9.0",
"jest": "^29.3.1", "jest-circus": "^24.9.0",
"jest-circus": "^29.3.1", "prettier": "1.19.1",
"prettier": "2.8.0", "ts-jest": "^24.2.0",
"ts-jest": "^29.0.3", "typescript": "^3.7.4",
"typescript": "^4.9.3",
"typescript-formatter": "^7.2.2" "typescript-formatter": "^7.2.2"
} }
} }

View File

@ -1,17 +1,14 @@
import fetch from "node-fetch"; import { GitHub } from "@actions/github";
import { GitHub } from "@actions/github/lib/utils"; import { Config, releaseBody } from "./util";
import { Config, isTag, releaseBody } from "./util"; import { lstatSync, readFileSync } from "fs";
import { statSync, readFileSync } from "fs";
import { getType } from "mime"; import { getType } from "mime";
import { basename } from "path"; import { basename } from "path";
type GitHub = InstanceType<typeof GitHub>;
export interface ReleaseAsset { export interface ReleaseAsset {
name: string; name: string;
mime: string; mime: string;
size: number; size: number;
data: Buffer; file: Buffer;
} }
export interface Release { export interface Release {
@ -19,12 +16,8 @@ export interface Release {
upload_url: string; upload_url: string;
html_url: string; html_url: string;
tag_name: string; tag_name: string;
name: string | null; body: string;
body?: string | null | undefined;
target_commitish: string; target_commitish: string;
draft: boolean;
prerelease: boolean;
assets: Array<{ id: number; name: string }>;
} }
export interface Releaser { export interface Releaser {
@ -42,9 +35,6 @@ export interface Releaser {
body: string | undefined; body: string | undefined;
draft: boolean | undefined; draft: boolean | undefined;
prerelease: boolean | undefined; prerelease: boolean | undefined;
target_commitish: string | undefined;
discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
}): Promise<{ data: Release }>; }): Promise<{ data: Release }>;
updateRelease(params: { updateRelease(params: {
@ -57,8 +47,6 @@ export interface Releaser {
body: string | undefined; body: string | undefined;
draft: boolean | undefined; draft: boolean | undefined;
prerelease: boolean | undefined; prerelease: boolean | undefined;
discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
}): Promise<{ data: Release }>; }): Promise<{ data: Release }>;
allReleases(params: { allReleases(params: {
@ -78,7 +66,7 @@ export class GitHubReleaser implements Releaser {
repo: string; repo: string;
tag: string; tag: string;
}): Promise<{ data: Release }> { }): Promise<{ data: Release }> {
return this.github.rest.repos.getReleaseByTag(params); return this.github.repos.getReleaseByTag(params);
} }
createRelease(params: { createRelease(params: {
@ -89,11 +77,8 @@ export class GitHubReleaser implements Releaser {
body: string | undefined; body: string | undefined;
draft: boolean | undefined; draft: boolean | undefined;
prerelease: boolean | undefined; prerelease: boolean | undefined;
target_commitish: string | undefined;
discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
}): Promise<{ data: Release }> { }): Promise<{ data: Release }> {
return this.github.rest.repos.createRelease(params); return this.github.repos.createRelease(params);
} }
updateRelease(params: { updateRelease(params: {
@ -106,10 +91,8 @@ export class GitHubReleaser implements Releaser {
body: string | undefined; body: string | undefined;
draft: boolean | undefined; draft: boolean | undefined;
prerelease: boolean | undefined; prerelease: boolean | undefined;
discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
}): Promise<{ data: Release }> { }): Promise<{ data: Release }> {
return this.github.rest.repos.updateRelease(params); return this.github.repos.updateRelease(params);
} }
allReleases(params: { allReleases(params: {
@ -118,7 +101,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.repos.listReleases.endpoint.merge(updatedParams)
); );
} }
} }
@ -127,8 +110,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), file: readFileSync(path)
}; };
}; };
@ -137,76 +120,38 @@ export const mimeOrDefault = (path: string): string => {
}; };
export const upload = async ( export const upload = async (
config: Config, gh: GitHub,
github: GitHub,
url: string, url: string,
path: string, path: string
currentAssets: Array<{ id: number; name: string }>
): Promise<any> => { ): Promise<any> => {
const [owner, repo] = config.github_repository.split("/"); let { name, size, mime, file } = asset(path);
const { name, size, mime, data: body } = asset(path);
const currentAsset = currentAssets.find(
({ name: currentName }) => currentName == name
);
if (currentAsset) {
console.log(`♻️ Deleting previously uploaded asset ${name}...`);
await github.rest.repos.deleteReleaseAsset({
asset_id: currentAsset.id || 1,
owner,
repo,
});
}
console.log(`⬆️ Uploading ${name}...`); console.log(`⬆️ Uploading ${name}...`);
const endpoint = new URL(url); return await gh.repos.uploadReleaseAsset({
endpoint.searchParams.append("name", name); url,
const resp = await fetch(endpoint, {
headers: { headers: {
"content-length": `${size}`, "content-length": size,
"content-type": mime, "content-type": mime
authorization: `token ${config.github_token}`,
}, },
method: "POST", name,
body, file
}); });
const json = await resp.json();
if (resp.status !== 201) {
throw new Error(
`Failed to upload release asset ${name}. received status code ${
resp.status
}\n${json.message}\n${JSON.stringify(json.errors)}`
);
}
return json;
}; };
export const release = async ( export const release = async (
config: Config, config: Config,
releaser: Releaser, releaser: Releaser
maxRetries: number = 3
): Promise<Release> => { ): Promise<Release> => {
if (maxRetries <= 0) {
console.log(`❌ Too many retries. Aborting...`);
throw new Error("Too many retries.");
}
const [owner, repo] = config.github_repository.split("/"); const [owner, repo] = config.github_repository.split("/");
const tag = const tag = config.github_ref.replace("refs/tags/", "");
config.input_tag_name ||
(isTag(config.github_ref)
? config.github_ref.replace("refs/tags/", "")
: "");
const discussion_category_name = config.input_discussion_category_name;
const generate_release_notes = config.input_generate_release_notes;
try { try {
// you can't get a 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
if (config.input_draft) { if (config.input_draft) {
for await (const response of releaser.allReleases({ for await (const response of releaser.allReleases({
owner, owner,
repo, repo
})) { })) {
let release = response.data.find((release) => release.tag_name === tag); let release = response.data.find(release => release.tag_name === tag);
if (release) { if (release) {
return release; return release;
} }
@ -215,46 +160,16 @@ export const release = async (
let existingRelease = await releaser.getReleaseByTag({ let existingRelease = await releaser.getReleaseByTag({
owner, owner,
repo, repo,
tag, tag
}); });
const release_id = existingRelease.data.id; const release_id = existingRelease.data.id;
let target_commitish: string; const target_commitish = existingRelease.data.target_commitish;
if (
config.input_target_commitish &&
config.input_target_commitish !== existingRelease.data.target_commitish
) {
console.log(
`Updating commit from "${existingRelease.data.target_commitish}" to "${config.input_target_commitish}"`
);
target_commitish = config.input_target_commitish;
} else {
target_commitish = existingRelease.data.target_commitish;
}
const tag_name = tag; const tag_name = tag;
const name = config.input_name || existingRelease.data.name || tag; const name = config.input_name || tag;
// revisit: support a new body-concat-strategy input for accumulating const body = `${existingRelease.data.body}\n${releaseBody(config)}`;
// body parts as a release gets updated. some users will likely want this while const draft = config.input_draft;
// others won't previously this was duplicating content for most which const prerelease = config.input_prerelease;
// no one wants
const workflowBody = releaseBody(config) || "";
const existingReleaseBody = existingRelease.data.body || "";
let body: string;
if (config.input_append_body && workflowBody && existingReleaseBody) {
body = existingReleaseBody + "\n" + workflowBody;
} else {
body = workflowBody || existingReleaseBody;
}
const draft =
config.input_draft !== undefined
? config.input_draft
: existingRelease.data.draft;
const prerelease =
config.input_prerelease !== undefined
? config.input_prerelease
: existingRelease.data.prerelease;
const release = await releaser.updateRelease({ const release = await releaser.updateRelease({
owner, owner,
@ -265,9 +180,7 @@ export const release = async (
name, name,
body, body,
draft, draft,
prerelease, prerelease
discussion_category_name,
generate_release_notes,
}); });
return release.data; return release.data;
} catch (error) { } catch (error) {
@ -277,14 +190,7 @@ export const release = async (
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; console.log(`👩‍🏭 Creating new GitHub release for tag ${tag_name}...`);
let commitMessage: string = "";
if (target_commitish) {
commitMessage = ` using commit "${target_commitish}"`;
}
console.log(
`👩‍🏭 Creating new GitHub release for tag ${tag_name}${commitMessage}...`
);
try { try {
let release = await releaser.createRelease({ let release = await releaser.createRelease({
owner, owner,
@ -293,22 +199,15 @@ export const release = async (
name, name,
body, body,
draft, draft,
prerelease, prerelease
target_commitish,
discussion_category_name,
generate_release_notes,
}); });
return release.data; return release.data;
} catch (error) { } catch (error) {
// presume a race with competing metrix runs // presume a race with competing metrix runs
console.log( console.log(
`⚠️ GitHub release failed with status: ${ `⚠️ GitHub release failed with status: ${error.status}, retrying...`
error.status
}\n${JSON.stringify(error.response.data.errors)}\nretrying... (${
maxRetries - 1
} retries remaining)`
); );
return release(config, releaser, maxRetries - 1); return release(config, releaser);
} }
} else { } else {
console.log( console.log(

View File

@ -1,92 +1,46 @@
import { import { paths, parseConfig, isTag } from "./util";
paths,
parseConfig,
isTag,
unmatchedPatterns,
uploadUrl,
} from "./util";
import { release, upload, GitHubReleaser } from "./github"; import { release, upload, GitHubReleaser } from "./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 { GitHub } from "@actions/github";
import { env } from "process"; import { env } from "process";
async function run() { async function run() {
try { try {
const config = parseConfig(env); const config = parseConfig(env);
if ( if (!isTag(config.github_ref)) {
!config.input_tag_name &&
!isTag(config.github_ref) &&
!config.input_draft
) {
throw new Error(`⚠️ GitHub Releases requires a tag`); throw new Error(`⚠️ GitHub Releases requires a tag`);
} }
if (config.input_files) { GitHub.plugin(require("@octokit/plugin-throttling"));
const patterns = unmatchedPatterns(config.input_files); const gh = new GitHub(config.github_token, {
patterns.forEach((pattern) => onRateLimit: (retryAfter, options) => {
console.warn(`🤔 Pattern '${pattern}' does not match any files.`) console.warn(
); `Request quota exhausted for request ${options.method} ${options.url}`
if (patterns.length > 0 && config.input_fail_on_unmatched_files) { );
throw new Error(`⚠️ There were unmatched files`); if (options.request.retryCount === 0) {
} // only retries once
} console.log(`Retrying after ${retryAfter} seconds!`);
return true;
// const oktokit = GitHub.plugin( }
// require("@octokit/plugin-throttling"),
// require("@octokit/plugin-retry")
// );
const gh = getOctokit(config.github_token, {
//new oktokit(
throttle: {
onRateLimit: (retryAfter, options) => {
console.warn(
`Request quota exhausted for request ${options.method} ${options.url}`
);
if (options.request.retryCount === 0) {
// only retries once
console.log(`Retrying after ${retryAfter} seconds!`);
return true;
}
},
onAbuseLimit: (retryAfter, options) => {
// does not retry, only logs a warning
console.warn(
`Abuse detected for request ${options.method} ${options.url}`
);
},
}, },
onAbuseLimit: (retryAfter, options) => {
// does not retry, only logs a warning
console.warn(
`Abuse detected for request ${options.method} ${options.url}`
);
}
}); });
//); let rel = await release(config, new GitHubReleaser(gh));
const rel = await release(config, new GitHubReleaser(gh));
if (config.input_files) { if (config.input_files) {
const files = paths(config.input_files); const files = paths(config.input_files);
if (files.length == 0) { if (files.length == 0) {
console.warn(`🤔 ${config.input_files} not include valid file.`); console.warn(`🤔 ${config.input_files} not include valid file.`);
} }
const currentAssets = rel.assets; files.forEach(async path => {
const assets = await Promise.all( await upload(gh, rel.upload_url, path);
files.map(async (path) => {
const json = await upload(
config,
gh,
uploadUrl(rel.upload_url),
path,
currentAssets
);
delete json.uploader;
return json;
})
).catch((error) => {
throw error;
}); });
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);
setOutput("id", rel.id.toString());
setOutput("upload_url", rel.upload_url);
} catch (error) { } catch (error) {
setFailed(error.message); setFailed(error.message);
} }

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;
@ -7,33 +7,18 @@ export interface Config {
github_repository: string; github_repository: string;
// user provided // user provided
input_name?: string; input_name?: string;
input_tag_name?: string;
input_repository?: string;
input_body?: string; input_body?: string;
input_body_path?: string; input_body_path?: string;
input_files?: string[]; input_files?: string[];
input_draft?: boolean; input_draft?: boolean;
input_prerelease?: boolean; input_prerelease?: boolean;
input_fail_on_unmatched_files?: boolean;
input_target_commitish?: string;
input_discussion_category_name?: string;
input_generate_release_notes?: boolean;
input_append_body?: boolean;
} }
export const uploadUrl = (url: string): string => {
const templateMarkerPos = url.indexOf("{");
if (templateMarkerPos > -1) {
return url.substring(0, templateMarkerPos);
}
return url;
};
export const releaseBody = (config: Config): string | undefined => { export const releaseBody = (config: Config): string | undefined => {
return ( return (
config.input_body ||
(config.input_body_path && (config.input_body_path &&
readFileSync(config.input_body_path).toString("utf8")) || readFileSync(config.input_body_path).toString("utf8"))
config.input_body
); );
}; };
@ -44,49 +29,30 @@ 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()),
[] []
); );
}; };
export const parseConfig = (env: Env): Config => { export const parseConfig = (env: Env): Config => {
return { return {
github_token: env.GITHUB_TOKEN || env.INPUT_TOKEN || "", github_token: env.GITHUB_TOKEN || "",
github_ref: env.GITHUB_REF || "", github_ref: env.GITHUB_REF || "",
github_repository: env.INPUT_REPOSITORY || env.GITHUB_REPOSITORY || "", github_repository: env.GITHUB_REPOSITORY || "",
input_name: env.INPUT_NAME, input_name: env.INPUT_NAME,
input_tag_name: env.INPUT_TAG_NAME?.trim(),
input_body: env.INPUT_BODY, input_body: env.INPUT_BODY,
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 === "true",
input_prerelease: env.INPUT_PRERELEASE input_prerelease: env.INPUT_PRERELEASE == "true"
? env.INPUT_PRERELEASE == "true"
: undefined,
input_fail_on_unmatched_files: env.INPUT_FAIL_ON_UNMATCHED_FILES == "true",
input_target_commitish: env.INPUT_TARGET_COMMITISH || undefined,
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",
}; };
}; };
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())
);
}, []);
};
export const unmatchedPatterns = (patterns: string[]): string[] => {
return patterns.reduce((acc: string[], pattern: string): string[] => {
return acc.concat(
glob.sync(pattern).filter((path) => statSync(path).isFile()).length == 0
? [pattern]
: []
); );
}, []); }, []);
}; };

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'. */