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
15 changed files with 366 additions and 3303 deletions

View File

@ -1,81 +1,25 @@
- 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:
@ -87,13 +31,13 @@ GitHub actions inputs don't inherently support lists of things and one might lik
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
@ -101,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

@ -2,11 +2,11 @@
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,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@v2 uses: actions/checkout@v1
- 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/')
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@v2 uses: actions/checkout@v1
- name: Release - name: Release
uses: softprops/action-gh-release@v1 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,7 +94,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v1
- name: Build - name: Build
run: echo ${{ github.sha }} > Release.txt run: echo ${{ github.sha }} > Release.txt
- name: Test - name: Test
@ -98,6 +104,8 @@ jobs:
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,7 +120,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v1
- name: Build - name: Build
run: echo ${{ github.sha }} > Release.txt run: echo ${{ github.sha }} > Release.txt
- name: Test - name: Test
@ -124,6 +132,8 @@ jobs:
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)
@ -144,19 +154,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 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@v1 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
@ -166,61 +173,36 @@ 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. |
| `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 |
💡 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/reference/repos#upload-a-release-asset--code-samples) (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
```
[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,192 +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({
// 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_repository: "",
github_token: "",
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_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_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_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_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_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_ref: "",
github_repository: "", github_repository: "",
github_token: "", github_token: "",
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: false,
input_files: [], input_files: [],
input_name: undefined, 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", () => {
@ -286,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,62 +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 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: "node12" using: 'node12'
main: "dist/index.js" main: 'dist/index.js'
branding: branding:
color: "green" color: 'green'
icon: "package" icon: 'package'

1139
dist/contextify.js vendored

File diff suppressed because it is too large Load Diff

83
dist/fixasync.js vendored
View File

@ -1,83 +0,0 @@
'use strict';
// eslint-disable-next-line no-invalid-this, no-shadow
const {GeneratorFunction, AsyncFunction, AsyncGeneratorFunction, global, internal, host, hook} = this;
const {Contextify, Decontextify} = internal;
// eslint-disable-next-line no-shadow
const {Function, eval: eval_, Promise, Object, Reflect} = global;
const {getOwnPropertyDescriptor, defineProperty, assign} = Object;
const {apply: rApply, construct: rConstruct} = Reflect;
const FunctionHandler = {
__proto__: null,
apply(target, thiz, args) {
const type = this.type;
args = Decontextify.arguments(args);
try {
args = Contextify.value(hook(type, args));
} catch (e) {
throw Contextify.value(e);
}
return rApply(target, thiz, args);
},
construct(target, args, newTarget) {
const type = this.type;
args = Decontextify.arguments(args);
try {
args = Contextify.value(hook(type, args));
} catch (e) {
throw Contextify.value(e);
}
return rConstruct(target, args, newTarget);
}
};
function makeCheckFunction(type) {
return assign({
__proto__: null,
type
}, FunctionHandler);
}
function override(obj, prop, value) {
const desc = getOwnPropertyDescriptor(obj, prop);
desc.value = value;
defineProperty(obj, prop, desc);
}
const proxiedFunction = new host.Proxy(Function, makeCheckFunction('function'));
override(Function.prototype, 'constructor', proxiedFunction);
if (GeneratorFunction) {
Object.setPrototypeOf(GeneratorFunction, proxiedFunction);
override(GeneratorFunction.prototype, 'constructor', new host.Proxy(GeneratorFunction, makeCheckFunction('generator_function')));
}
if (AsyncFunction) {
Object.setPrototypeOf(AsyncFunction, proxiedFunction);
override(AsyncFunction.prototype, 'constructor', new host.Proxy(AsyncFunction, makeCheckFunction('async_function')));
}
if (AsyncGeneratorFunction) {
Object.setPrototypeOf(AsyncGeneratorFunction, proxiedFunction);
override(AsyncGeneratorFunction.prototype, 'constructor', new host.Proxy(AsyncGeneratorFunction, makeCheckFunction('async_generator_function')));
}
global.Function = proxiedFunction;
global.eval = new host.Proxy(eval_, makeCheckFunction('eval'));
if (Promise) {
Promise.prototype.then = new host.Proxy(Promise.prototype.then, makeCheckFunction('promise_then'));
// This seems not to work, and will produce
// UnhandledPromiseRejectionWarning: TypeError: Method Promise.prototype.then called on incompatible receiver [object Object].
// This is likely caused since the host.Promise.prototype.then cannot use the VM Proxy object.
// Contextify.connect(host.Promise.prototype.then, Promise.prototype.then);
if (Promise.prototype.finally) {
Promise.prototype.finally = new host.Proxy(Promise.prototype.finally, makeCheckFunction('promise_finally'));
// Contextify.connect(host.Promise.prototype.finally, Promise.prototype.finally);
}
if (Promise.prototype.catch) {
Promise.prototype.catch = new host.Proxy(Promise.prototype.catch, makeCheckFunction('promise_catch'));
// Contextify.connect(host.Promise.prototype.catch, Promise.prototype.catch);
}
}

136
dist/index.js vendored

File diff suppressed because one or more lines are too long

682
dist/sandbox.js vendored
View File

@ -1,682 +0,0 @@
/* eslint-disable no-shadow, no-invalid-this */
/* global vm, host, Contextify, Decontextify, VMError, options */
'use strict';
const {Script} = host.require('vm');
const fs = host.require('fs');
const pa = host.require('path');
const BUILTIN_MODULES = host.process.binding('natives');
const parseJSON = JSON.parse;
const importModuleDynamically = () => {
// We can't throw an error object here because since vm.Script doesn't store a context, we can't properly contextify that error object.
// eslint-disable-next-line no-throw-literal
throw 'Dynamic imports are not allowed.';
};
/**
* @param {Object} host Hosts's internal objects.
*/
return ((vm, host) => {
'use strict';
const global = this;
const TIMERS = new host.WeakMap(); // Contains map of timers created inside sandbox
const BUILTINS = {__proto__: null};
const CACHE = {__proto__: null};
const EXTENSIONS = {
__proto__: null,
['.json'](module, filename) {
try {
const code = fs.readFileSync(filename, 'utf8');
module.exports = parseJSON(code);
} catch (e) {
throw Contextify.value(e);
}
},
['.node'](module, filename) {
if (vm.options.require.context === 'sandbox') throw new VMError('Native modules can be required only with context set to \'host\'.');
try {
module.exports = Contextify.readonly(host.require(filename));
} catch (e) {
throw Contextify.value(e);
}
}
};
for (let i = 0; i < vm.options.sourceExtensions.length; i++) {
const ext = vm.options.sourceExtensions[i];
EXTENSIONS['.' + ext] = (module, filename, dirname) => {
if (vm.options.require.context !== 'sandbox') {
try {
module.exports = Contextify.readonly(host.require(filename));
} catch (e) {
throw Contextify.value(e);
}
} else {
let script;
try {
// Load module
let contents = fs.readFileSync(filename, 'utf8');
contents = vm._compiler(contents, filename);
const code = host.STRICT_MODULE_PREFIX + contents + host.MODULE_SUFFIX;
const ccode = vm._hook('run', [code]);
// Precompile script
script = new Script(ccode, {
__proto__: null,
filename: filename || 'vm.js',
displayErrors: false,
importModuleDynamically
});
} catch (ex) {
throw Contextify.value(ex);
}
const closure = script.runInContext(global, {
__proto__: null,
filename: filename || 'vm.js',
displayErrors: false,
importModuleDynamically
});
// run the script
closure(module.exports, module.require, module, filename, dirname);
}
};
}
const _parseExternalOptions = (options) => {
if (host.Array.isArray(options)) {
return {
__proto__: null,
external: options,
transitive: false
};
}
return {
__proto__: null,
external: options.modules,
transitive: options.transitive
};
};
/**
* Resolve filename.
*/
const _resolveFilename = (path) => {
if (!path) return null;
let hasPackageJson;
try {
path = pa.resolve(path);
const exists = fs.existsSync(path);
const isdir = exists ? fs.statSync(path).isDirectory() : false;
// direct file match
if (exists && !isdir) return path;
// load as file
for (let i = 0; i < vm.options.sourceExtensions.length; i++) {
const ext = vm.options.sourceExtensions[i];
if (fs.existsSync(`${path}.${ext}`)) return `${path}.${ext}`;
}
if (fs.existsSync(`${path}.json`)) return `${path}.json`;
if (fs.existsSync(`${path}.node`)) return `${path}.node`;
// load as module
hasPackageJson = fs.existsSync(`${path}/package.json`);
} catch (e) {
throw Contextify.value(e);
}
if (hasPackageJson) {
let pkg;
try {
pkg = fs.readFileSync(`${path}/package.json`, 'utf8');
} catch (e) {
throw Contextify.value(e);
}
try {
pkg = parseJSON(pkg);
} catch (ex) {
throw new VMError(`Module '${path}' has invalid package.json`, 'EMODULEINVALID');
}
let main;
if (pkg && pkg.main) {
main = _resolveFilename(`${path}/${pkg.main}`);
if (!main) main = _resolveFilename(`${path}/index`);
} else {
main = _resolveFilename(`${path}/index`);
}
return main;
}
// load as directory
try {
for (let i = 0; i < vm.options.sourceExtensions.length; i++) {
const ext = vm.options.sourceExtensions[i];
if (fs.existsSync(`${path}/index.${ext}`)) return `${path}/index.${ext}`;
}
if (fs.existsSync(`${path}/index.json`)) return `${path}/index.json`;
if (fs.existsSync(`${path}/index.node`)) return `${path}/index.node`;
} catch (e) {
throw Contextify.value(e);
}
return null;
};
/**
* Builtin require.
*/
const _requireBuiltin = (moduleName) => {
if (moduleName === 'buffer') return ({Buffer});
if (BUILTINS[moduleName]) return BUILTINS[moduleName].exports; // Only compiled builtins are stored here
if (moduleName === 'util') {
return Contextify.readonly(host.require(moduleName), {
// Allows VM context to use util.inherits
__proto__: null,
inherits: (ctor, superCtor) => {
ctor.super_ = superCtor;
Object.setPrototypeOf(ctor.prototype, superCtor.prototype);
}
});
}
if (moduleName === 'events' || moduleName === 'internal/errors') {
let script;
try {
script = new Script(`(function (exports, require, module, process, internalBinding) {
'use strict';
const primordials = global;
${BUILTIN_MODULES[moduleName]}
\n
});`, {
filename: `${moduleName}.vm.js`
});
} catch (e) {
throw Contextify.value(e);
}
// setup module scope
const module = BUILTINS[moduleName] = {
exports: {},
require: _requireBuiltin
};
// run script
try {
// FIXME binding should be contextified
script.runInContext(global)(module.exports, module.require, module, host.process, host.process.binding);
} catch (e) {
// e could be from inside or outside of sandbox
throw new VMError(`Error loading '${moduleName}'`);
}
return module.exports;
}
return Contextify.readonly(host.require(moduleName));
};
/**
* Prepare require.
*/
const _prepareRequire = (currentDirname, parentAllowsTransitive = false) => {
const _require = moduleName => {
let requireObj;
try {
const optionsObj = vm.options;
if (optionsObj.nesting && moduleName === 'vm2') return {VM: Contextify.readonly(host.VM), NodeVM: Contextify.readonly(host.NodeVM)};
requireObj = optionsObj.require;
} catch (e) {
throw Contextify.value(e);
}
if (!requireObj) throw new VMError(`Access denied to require '${moduleName}'`, 'EDENIED');
if (moduleName == null) throw new VMError("Module '' not found.", 'ENOTFOUND');
if (typeof moduleName !== 'string') throw new VMError(`Invalid module name '${moduleName}'`, 'EINVALIDNAME');
let filename;
let allowRequireTransitive = false;
// Mock?
try {
const {mock} = requireObj;
if (mock) {
const mockModule = mock[moduleName];
if (mockModule) {
return Contextify.readonly(mockModule);
}
}
} catch (e) {
throw Contextify.value(e);
}
// Builtin?
if (BUILTIN_MODULES[moduleName]) {
let allowed;
try {
const builtinObj = requireObj.builtin;
if (host.Array.isArray(builtinObj)) {
if (builtinObj.indexOf('*') >= 0) {
allowed = builtinObj.indexOf(`-${moduleName}`) === -1;
} else {
allowed = builtinObj.indexOf(moduleName) >= 0;
}
} else if (builtinObj) {
allowed = builtinObj[moduleName];
} else {
allowed = false;
}
} catch (e) {
throw Contextify.value(e);
}
if (!allowed) throw new VMError(`Access denied to require '${moduleName}'`, 'EDENIED');
return _requireBuiltin(moduleName);
}
// External?
let externalObj;
try {
externalObj = requireObj.external;
} catch (e) {
throw Contextify.value(e);
}
if (!externalObj) throw new VMError(`Access denied to require '${moduleName}'`, 'EDENIED');
if (/^(\.|\.\/|\.\.\/)/.exec(moduleName)) {
// Module is relative file, e.g. ./script.js or ../script.js
if (!currentDirname) throw new VMError('You must specify script path to load relative modules.', 'ENOPATH');
filename = _resolveFilename(`${currentDirname}/${moduleName}`);
} else if (/^(\/|\\|[a-zA-Z]:\\)/.exec(moduleName)) {
// Module is absolute file, e.g. /script.js or //server/script.js or C:\script.js
filename = _resolveFilename(moduleName);
} else {
// Check node_modules in path
if (!currentDirname) throw new VMError('You must specify script path to load relative modules.', 'ENOPATH');
if (typeof externalObj === 'object') {
let isWhitelisted;
try {
const { external, transitive } = _parseExternalOptions(externalObj);
isWhitelisted = external.some(ext => host.helpers.match(ext, moduleName)) || (transitive && parentAllowsTransitive);
} catch (e) {
throw Contextify.value(e);
}
if (!isWhitelisted) {
throw new VMError(`The module '${moduleName}' is not whitelisted in VM.`, 'EDENIED');
}
allowRequireTransitive = true;
}
// FIXME the paths array has side effects
const paths = currentDirname.split(pa.sep);
while (paths.length) {
const path = paths.join(pa.sep);
// console.log moduleName, "#{path}#{pa.sep}node_modules#{pa.sep}#{moduleName}"
filename = _resolveFilename(`${path}${pa.sep}node_modules${pa.sep}${moduleName}`);
if (filename) break;
paths.pop();
}
}
if (!filename) {
let resolveFunc;
try {
resolveFunc = requireObj.resolve;
} catch (e) {
throw Contextify.value(e);
}
if (resolveFunc) {
let resolved;
try {
resolved = requireObj.resolve(moduleName, currentDirname);
} catch (e) {
throw Contextify.value(e);
}
filename = _resolveFilename(resolved);
}
}
if (!filename) throw new VMError(`Cannot find module '${moduleName}'`, 'ENOTFOUND');
// return cache whenever possible
if (CACHE[filename]) return CACHE[filename].exports;
const dirname = pa.dirname(filename);
const extname = pa.extname(filename);
let allowedModule = true;
try {
const rootObj = requireObj.root;
if (rootObj) {
const rootPaths = host.Array.isArray(rootObj) ? rootObj : host.Array.of(rootObj);
allowedModule = rootPaths.some(path => host.String.prototype.startsWith.call(dirname, pa.resolve(path)));
}
} catch (e) {
throw Contextify.value(e);
}
if (!allowedModule) {
throw new VMError(`Module '${moduleName}' is not allowed to be required. The path is outside the border!`, 'EDENIED');
}
const module = CACHE[filename] = {
filename,
exports: {},
require: _prepareRequire(dirname, allowRequireTransitive)
};
// lookup extensions
if (EXTENSIONS[extname]) {
EXTENSIONS[extname](module, filename, dirname);
return module.exports;
}
throw new VMError(`Failed to load '${moduleName}': Unknown type.`, 'ELOADFAIL');
};
return _require;
};
/**
* Prepare sandbox.
*/
// This is a function and not an arrow function, since the original is also a function
global.setTimeout = function setTimeout(callback, delay, ...args) {
if (typeof callback !== 'function') throw new TypeError('"callback" argument must be a function');
let tmr;
try {
tmr = host.setTimeout(Decontextify.value(() => {
// FIXME ...args has side effects
callback(...args);
}), Decontextify.value(delay));
} catch (e) {
throw Contextify.value(e);
}
const local = Contextify.value(tmr);
TIMERS.set(local, tmr);
return local;
};
global.setInterval = function setInterval(callback, interval, ...args) {
if (typeof callback !== 'function') throw new TypeError('"callback" argument must be a function');
let tmr;
try {
tmr = host.setInterval(Decontextify.value(() => {
// FIXME ...args has side effects
callback(...args);
}), Decontextify.value(interval));
} catch (e) {
throw Contextify.value(e);
}
const local = Contextify.value(tmr);
TIMERS.set(local, tmr);
return local;
};
global.setImmediate = function setImmediate(callback, ...args) {
if (typeof callback !== 'function') throw new TypeError('"callback" argument must be a function');
let tmr;
try {
tmr = host.setImmediate(Decontextify.value(() => {
// FIXME ...args has side effects
callback(...args);
}));
} catch (e) {
throw Contextify.value(e);
}
const local = Contextify.value(tmr);
TIMERS.set(local, tmr);
return local;
};
global.clearTimeout = function clearTimeout(local) {
try {
host.clearTimeout(TIMERS.get(local));
} catch (e) {
throw Contextify.value(e);
}
};
global.clearInterval = function clearInterval(local) {
try {
host.clearInterval(TIMERS.get(local));
} catch (e) {
throw Contextify.value(e);
}
};
global.clearImmediate = function clearImmediate(local) {
try {
host.clearImmediate(TIMERS.get(local));
} catch (e) {
throw Contextify.value(e);
}
};
function addListener(name, handler) {
if (name !== 'beforeExit' && name !== 'exit') {
throw new Error(`Access denied to listen for '${name}' event.`);
}
try {
host.process.on(name, Decontextify.value(handler));
} catch (e) {
throw Contextify.value(e);
}
return this;
}
const {argv: optionArgv, env: optionsEnv} = options;
// FIXME wrong class structure
global.process = {
argv: optionArgv !== undefined ? Contextify.value(optionArgv) : [],
title: host.process.title,
version: host.process.version,
versions: Contextify.readonly(host.process.versions),
arch: host.process.arch,
platform: host.process.platform,
env: optionsEnv !== undefined ? Contextify.value(optionsEnv) : {},
pid: host.process.pid,
features: Contextify.readonly(host.process.features),
nextTick: function nextTick(callback, ...args) {
if (typeof callback !== 'function') {
throw new Error('Callback must be a function.');
}
try {
host.process.nextTick(Decontextify.value(() => {
// FIXME ...args has side effects
callback(...args);
}));
} catch (e) {
throw Contextify.value(e);
}
},
hrtime: function hrtime(time) {
try {
return Contextify.value(host.process.hrtime(Decontextify.value(time)));
} catch (e) {
throw Contextify.value(e);
}
},
cwd: function cwd() {
try {
return Contextify.value(host.process.cwd());
} catch (e) {
throw Contextify.value(e);
}
},
addListener,
on: addListener,
once: function once(name, handler) {
if (name !== 'beforeExit' && name !== 'exit') {
throw new Error(`Access denied to listen for '${name}' event.`);
}
try {
host.process.once(name, Decontextify.value(handler));
} catch (e) {
throw Contextify.value(e);
}
return this;
},
listeners: function listeners(name) {
if (name !== 'beforeExit' && name !== 'exit') {
// Maybe add ({__proto__:null})[name] to throw when name fails in https://tc39.es/ecma262/#sec-topropertykey.
return [];
}
// Filter out listeners, which were not created in this sandbox
try {
return Contextify.value(host.process.listeners(name).filter(listener => Contextify.isVMProxy(listener)));
} catch (e) {
throw Contextify.value(e);
}
},
removeListener: function removeListener(name, handler) {
if (name !== 'beforeExit' && name !== 'exit') {
return this;
}
try {
host.process.removeListener(name, Decontextify.value(handler));
} catch (e) {
throw Contextify.value(e);
}
return this;
},
umask: function umask() {
if (arguments.length) {
throw new Error('Access denied to set umask.');
}
try {
return Contextify.value(host.process.umask());
} catch (e) {
throw Contextify.value(e);
}
}
};
if (vm.options.console === 'inherit') {
global.console = Contextify.readonly(host.console);
} else if (vm.options.console === 'redirect') {
global.console = {
debug(...args) {
try {
// FIXME ...args has side effects
vm.emit('console.debug', ...Decontextify.arguments(args));
} catch (e) {
throw Contextify.value(e);
}
},
log(...args) {
try {
// FIXME ...args has side effects
vm.emit('console.log', ...Decontextify.arguments(args));
} catch (e) {
throw Contextify.value(e);
}
},
info(...args) {
try {
// FIXME ...args has side effects
vm.emit('console.info', ...Decontextify.arguments(args));
} catch (e) {
throw Contextify.value(e);
}
},
warn(...args) {
try {
// FIXME ...args has side effects
vm.emit('console.warn', ...Decontextify.arguments(args));
} catch (e) {
throw Contextify.value(e);
}
},
error(...args) {
try {
// FIXME ...args has side effects
vm.emit('console.error', ...Decontextify.arguments(args));
} catch (e) {
throw Contextify.value(e);
}
},
dir(...args) {
try {
vm.emit('console.dir', ...Decontextify.arguments(args));
} catch (e) {
throw Contextify.value(e);
}
},
time() {},
timeEnd() {},
trace(...args) {
try {
// FIXME ...args has side effects
vm.emit('console.trace', ...Decontextify.arguments(args));
} catch (e) {
throw Contextify.value(e);
}
}
};
}
/*
Return contextified require.
*/
return _prepareRequire;
})(vm, host);

815
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,18 @@
{ {
"name": "action-gh-release", "name": "action-gh-release",
"version": "0.1.14", "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"
@ -20,21 +20,18 @@
"author": "softprops", "author": "softprops",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/action": "^3.18.0", "@actions/core": "^1.2.0",
"@actions/core": "^1.4.0", "@actions/github": "^2.0.0",
"@octokit/plugin-retry": "^3.0.9", "@octokit/plugin-throttling": "^2.7.1",
"@octokit/plugin-throttling": "^3.5.1",
"glob": "^7.1.6", "glob": "^7.1.6",
"mime": "^2.4.4", "mime": "^2.4.4"
"node-fetch": "^2.6.1"
}, },
"devDependencies": { "devDependencies": {
"@types/glob": "^7.1.1", "@types/glob": "^7.1.1",
"@types/jest": "^24.0.25", "@types/jest": "^24.0.25",
"@types/mime": "^2.0.1", "@types/mime": "^2.0.1",
"@types/node": "^12.12.24", "@types/node": "^12.12.24",
"@types/node-fetch": "^2.5.12", "@zeit/ncc": "^0.21.0",
"@vercel/ncc": "^0.33.0",
"jest": "^24.9.0", "jest": "^24.9.0",
"jest-circus": "^24.9.0", "jest-circus": "^24.9.0",
"prettier": "1.19.1", "prettier": "1.19.1",

View File

@ -1,17 +1,14 @@
import fetch from "node-fetch"; import { GitHub } from "@actions/github";
import { Octokit } from "@octokit/action"; 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 Octokit>;
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,67 +120,29 @@ 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
@ -219,35 +164,12 @@ export const release = async (
}); });
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
let body = releaseBody(config) || existingRelease.data.body || "";
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,
@ -258,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) {
@ -270,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,
@ -286,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,41 +1,17 @@
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 { Octokit } from "@octokit/action";
import { setFailed, setOutput } from "@actions/core"; import { setFailed, setOutput } from "@actions/core";
import { retry } from "@octokit/plugin-retry"; import { GitHub } from "@actions/github";
import { throttling } from "@octokit/plugin-throttling";
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 =>
console.warn(`🤔 Pattern '${pattern}' does not match any files.`)
);
if (patterns.length > 0 && config.input_fail_on_unmatched_files) {
throw new Error(`⚠️ There were unmatched files`);
}
}
const OctokitWithPlugins = Octokit.plugin(retry, throttling);
const gh = new OctokitWithPlugins({
auth: config.github_token,
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}`
@ -52,37 +28,19 @@ async function run() {
`Abuse detected for request ${options.method} ${options.url}` `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,32 +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;
} }
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
); );
}; };
@ -51,40 +37,22 @@ export const parseInputFiles = (files: string): string[] => {
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"
}; };
}; };
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]
: []
); );
}, []); }, []);
}; };