Compare commits

..
Author SHA1 Message Date
softprops e6268c631a error is of unknown structure 2021-08-09 09:19:25 -04:00
softprops 8988630456 error is of unknown structure 2021-08-09 00:20:24 -04:00
softprops c1b107442c error is of unknown structure 2021-08-09 00:18:15 -04:00
softprops 4c8c431191 error is of unknown structure 2021-08-09 00:16:19 -04:00
softprops 1505034bb0 error is of unknown structure 2021-08-09 00:14:58 -04:00
softprops 21e9098c3b more error info 2021-08-09 00:12:44 -04:00
softprops 26941a6e6b debug cross repo 2021-08-09 00:05:07 -04:00
25 changed files with 5604 additions and 5356 deletions

No files matched your search

-97
View File
@@ -1,97 +0,0 @@
name: Bug report
description: Report a bug or regression in action-gh-release
title: "[Bug]: "
labels:
- bug
body:
- type: markdown
attributes:
value: |
Before filing:
- confirm the problem still reproduces on the latest release or `master`
- search existing issues for the same behavior
- if the original repository is private, include a minimal public repro, a sanitized workflow snippet, or exact redacted steps a maintainer can follow
- type: checkboxes
id: checks
attributes:
label: Pre-flight checks
options:
- label: I searched existing issues and did not find a duplicate
required: true
- label: I reproduced this with the latest released version or current `master`
required: true
- label: I included a reproducible example or a sanitized/redacted reproduction path if the original repository is private
required: true
- type: input
id: action_version
attributes:
label: action-gh-release version
description: Tag, SHA, or ref used in your workflow
placeholder: v2.5.2
validations:
required: true
- type: dropdown
id: runner
attributes:
label: Runner operating system
options:
- ubuntu-latest
- windows-latest
- macos-latest
- other
validations:
required: true
- type: input
id: target_repository
attributes:
label: Release target repository
description: Fill this in if you set the `repository:` input
placeholder: owner/repo
- type: input
id: repro_reference
attributes:
label: Reproduction repo, gist, or artifact
description: Link a minimal repro repository, gist, run URL, or other shareable artifact if you have one
placeholder: https://github.com/owner/repro-repo
- type: textarea
id: workflow
attributes:
label: Workflow snippet
description: Include the relevant `uses:` step and inputs. If the original repo is private, paste a sanitized version here.
render: yaml
- type: textarea
id: expected
attributes:
label: Expected behavior
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual behavior
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: Include tags, matrix/concurrency details, and any repo rules involved. If the original repo is private, describe the smallest setup a maintainer can recreate locally or in a throwaway repo.
placeholder: |
1. Trigger workflow with ...
2. Action creates ...
3. Action fails with ...
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant logs
description: Paste the relevant error output or run URL
render: shell
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: Any extra environment, token, ruleset, or asset details
@@ -1,50 +0,0 @@
name: Feature request
description: Propose an enhancement or new capability for action-gh-release
title: "[Feature]: "
labels:
- enhancement
body:
- type: markdown
attributes:
value: |
Use this template for new capabilities, behavior changes, or ergonomics improvements.
If you are reporting something broken, use the bug report template instead.
- type: checkboxes
id: checks
attributes:
label: Pre-flight checks
options:
- label: I searched existing issues and did not find a duplicate request
required: true
- label: This is not a bug report for existing behavior
required: true
- type: textarea
id: problem
attributes:
label: Problem to solve
description: What workflow pain point or gap are you trying to address?
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed solution
description: Describe the behavior, input, or output you want
validations:
required: true
- type: textarea
id: workflow
attributes:
label: Example workflow snippet
description: Show how you would expect to use this
render: yaml
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Workarounds or other approaches you evaluated
- type: textarea
id: impact
attributes:
label: Why this belongs in action-gh-release
description: Explain the user impact or why this should live in the action rather than in workflow glue
-29
View File
@@ -1,29 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
groups:
npm:
patterns:
- "*"
ignore:
- dependency-name: node-fetch
versions:
- ">=3.0.0"
- dependency-name: "@types/node"
versions:
- ">=22.0.0"
commit-message:
prefix: "chore(deps)"
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
groups:
github-actions:
patterns:
- "*"
commit-message:
prefix: "chore(deps)"
-22
View File
@@ -1,22 +0,0 @@
changelog:
exclude:
labels:
- ignore-for-release
- github-actions
authors:
- octocat
- renovate[bot]
categories:
- title: Breaking Changes 🛠
labels:
- breaking-change
- title: Exciting New Features 🎉
labels:
- enhancement
- feature
- title: Bug fixes 🐛
labels:
- bug
- title: Other Changes 🔄
labels:
- "*"
+6 -12
View File
@@ -1,20 +1,14 @@
name: main
name: Main
on:
push:
pull_request:
on: [pull_request, push]
jobs:
build:
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version-file: ".tool-versions"
cache: "npm"
# https://github.com/actions/checkout
- name: Checkout
uses: actions/checkout@v2
- name: Install
run: npm ci
- name: Build
-1
View File
@@ -2,4 +2,3 @@ __tests__/runner/*
# actions requires a node_modules dir https://github.com/actions/toolkit/blob/master/docs/javascript-action.md#publish-a-releasesv1-action
# but its recommended not to check these in https://github.com/actions/toolkit/blob/master/docs/action-versioning.md#recommendations
node_modules
coverage
-16
View File
@@ -1,16 +0,0 @@
# Build outputs
dist/
lib/
coverage/
# Dependencies
node_modules/
# Misc
.github/
*.log
.DS_Store
__tests__/release.txt
# Package files
package-lock.json
-11
View File
@@ -1,11 +0,0 @@
/**
* @type {import('prettier').Config}
*/
module.exports = {
trailingComma: 'all',
tabWidth: 2,
semi: true,
singleQuote: true,
printWidth: 100,
bracketSpacing: true,
};
-1
View File
@@ -1 +0,0 @@
nodejs 24.11.0
+7 -272
View File
@@ -1,268 +1,3 @@
## 2.5.2
`2.5.2` is a patch release focused on the remaining release-creation and prerelease regressions in the `2.5.x` bug-fix cycle.
It fixes `#705`, fixes `#708`, fixes `#740`, fixes `#741`, and fixes `#722`.
Regression testing covers the shared-tag race, prerelease event behavior, dotfile asset labels,
same-filename concurrent uploads, and blocked-tag cleanup behavior.
If you still hit an issue after upgrading, please open a report with the bug template and include a minimal repro or sanitized workflow snippet where possible.
## What's Changed
### Bug fixes 🐛
* fix: canonicalize releases after concurrent create by @chenrui333 in https://github.com/softprops/action-gh-release/pull/746
* fix: preserve prereleased events for prereleases by @chenrui333 in https://github.com/softprops/action-gh-release/pull/748
* fix: restore dotfile asset labels by @chenrui333 in https://github.com/softprops/action-gh-release/pull/749
* fix: handle upload already_exists races across workflows by @api2062 in https://github.com/softprops/action-gh-release/pull/745
* fix: clean up orphan drafts when tag creation is blocked by @chenrui333 in https://github.com/softprops/action-gh-release/pull/750
## 2.5.1
`2.5.1` is a patch release focused on regressions introduced in `2.5.0` and on release lookup reliability.
It fixes `#713`, addresses `#703`, and fixes `#724`. Regression testing shows that
current `master` no longer reproduces the finalize-race behavior reported in `#704` and `#709`.
## What's Changed
### Bug fixes 🐛
* fix: fetch correct asset URL after finalization; test; some refactoring by @pzhlkj6612 in https://github.com/softprops/action-gh-release/pull/738
* fix: release marked as 'latest' despite make_latest: false by @Boshen in https://github.com/softprops/action-gh-release/pull/715
* fix: use getReleaseByTag API instead of iterating all releases by @kim-em in https://github.com/softprops/action-gh-release/pull/725
### Other Changes 🔄
* dependency updates, including the ESM/runtime compatibility refresh in https://github.com/softprops/action-gh-release/pull/731
## 2.5.0
## What's Changed
### Exciting New Features 🎉
* feat: mark release as draft until all artifacts are uploaded by @dumbmoron in https://github.com/softprops/action-gh-release/pull/692
### Other Changes 🔄
* dependency updates
## 2.4.2
## What's Changed
### Exciting New Features 🎉
* feat: Ensure generated release notes cannot be over 125000 characters by @BeryJu in https://github.com/softprops/action-gh-release/pull/684
### Other Changes 🔄
* dependency updates
## 2.4.1
## What's Changed
### Other Changes 🔄
* fix(util): support brace expansion globs containing commas in parseInputFiles by @Copilot in https://github.com/softprops/action-gh-release/pull/672
* fix: gracefully fallback to body when body_path cannot be read by @Copilot in https://github.com/softprops/action-gh-release/pull/671
## 2.4.0
## What's Changed
### Exciting New Features 🎉
* feat(action): respect working_directory for files globs by @stephenway in https://github.com/softprops/action-gh-release/pull/667
## 2.3.4
## What's Changed
### Bug fixes 🐛
* fix(action): handle 422 already_exists race condition by @stephenway in https://github.com/softprops/action-gh-release/pull/665
### Other Changes 🔄
- dependency updates
## 2.3.3
## What's Changed
### Exciting New Features 🎉
* feat: add input option `overwrite_files` by @asfernandes in https://github.com/softprops/action-gh-release/pull/343
### Other Changes 🔄
- dependency updates
## 2.3.2
* fix: revert fs `readableWebStream` change
## 2.3.1
### Bug fixes 🐛
* fix: fix file closing issue by @WailGree in https://github.com/softprops/action-gh-release/pull/629
## 2.3.0
* Migrate from jest to vitest
* Replace `mime` with `mime-types`
* Bump to use node 24
* Dependency updates
## 2.2.2
## What's Changed
### Bug fixes 🐛
* fix: updating release draft status from true to false by @galargh in https://github.com/softprops/action-gh-release/pull/316
### Other Changes 🔄
* chore: simplify ref_type test by @steinybot in https://github.com/softprops/action-gh-release/pull/598
* fix(docs): clarify the default for tag_name by @muzimuzhi in https://github.com/softprops/action-gh-release/pull/599
* test(release): add unit tests when searching for a release by @rwaskiewicz in https://github.com/softprops/action-gh-release/pull/603
* dependency updates
## 2.2.1
## What's Changed
### Bug fixes 🐛
* fix: big file uploads by @xen0n in https://github.com/softprops/action-gh-release/pull/562
### Other Changes 🔄
* chore(deps): bump @types/node from 22.10.1 to 22.10.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/559
* chore(deps): bump @types/node from 22.10.2 to 22.10.5 by @dependabot in https://github.com/softprops/action-gh-release/pull/569
* chore: update error and warning messages for not matching files in files field by @ytimocin in https://github.com/softprops/action-gh-release/pull/568
## 2.2.0
## What's Changed
### Exciting New Features 🎉
* feat: read the release assets asynchronously by @xen0n in https://github.com/softprops/action-gh-release/pull/552
### Bug fixes 🐛
* fix(docs): clarify the default for tag_name by @alexeagle in https://github.com/softprops/action-gh-release/pull/544
### Other Changes 🔄
* chore(deps): bump typescript from 5.6.3 to 5.7.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/548
* chore(deps): bump @types/node from 22.9.0 to 22.9.4 by @dependabot in https://github.com/softprops/action-gh-release/pull/547
* chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 by @dependabot in https://github.com/softprops/action-gh-release/pull/545
* chore(deps): bump @vercel/ncc from 0.38.2 to 0.38.3 by @dependabot in https://github.com/softprops/action-gh-release/pull/543
* chore(deps): bump prettier from 3.3.3 to 3.4.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/550
* chore(deps): bump @types/node from 22.9.4 to 22.10.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/551
* chore(deps): bump prettier from 3.4.1 to 3.4.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/554
## 2.1.0
## What's Changed
### Exciting New Features 🎉
* feat: add support for release assets with multiple spaces within the name by @dukhine in https://github.com/softprops/action-gh-release/pull/518
* feat: preserve upload order by @richarddd in https://github.com/softprops/action-gh-release/pull/500
### Other Changes 🔄
* chore(deps): bump @types/node from 22.8.2 to 22.8.7 by @dependabot in https://github.com/softprops/action-gh-release/pull/539
## 2.0.9
- maintenance release with updated dependencies
## 2.0.8
### Other Changes 🔄
* chore(deps): bump prettier from 2.8.0 to 3.3.3 by @dependabot in https://github.com/softprops/action-gh-release/pull/480
* chore(deps): bump @types/node from 20.14.9 to 20.14.11 by @dependabot in https://github.com/softprops/action-gh-release/pull/483
* chore(deps): bump @octokit/plugin-throttling from 9.3.0 to 9.3.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/484
* chore(deps): bump glob from 10.4.2 to 11.0.0 by @dependabot in https://github.com/softprops/action-gh-release/pull/477
* refactor: write jest config in ts by @chenrui333 in https://github.com/softprops/action-gh-release/pull/485
* chore(deps): bump @actions/github from 5.1.1 to 6.0.0 by @dependabot in https://github.com/softprops/action-gh-release/pull/470
## 2.0.7
### Bug fixes 🐛
* Fix missing update release body by @FirelightFlagboy in https://github.com/softprops/action-gh-release/pull/365
### Other Changes 🔄
* Bump @octokit/plugin-retry from 4.0.3 to 7.1.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/443
* Bump typescript from 4.9.5 to 5.5.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/467
* Bump @types/node from 20.14.6 to 20.14.8 by @dependabot in https://github.com/softprops/action-gh-release/pull/469
* Bump @types/node from 20.14.8 to 20.14.9 by @dependabot in https://github.com/softprops/action-gh-release/pull/473
* Bump typescript from 5.5.2 to 5.5.3 by @dependabot in https://github.com/softprops/action-gh-release/pull/472
* Bump ts-jest from 29.1.5 to 29.2.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/479
* docs: document that existing releases are updated by @jvanbruegge in https://github.com/softprops/action-gh-release/pull/474
## 2.0.6
- maintenance release with updated dependencies
## 2.0.5
- Factor in file names with spaces when upserting files [#446](https://github.com/softprops/action-gh-release/pull/446) via [@MystiPanda](https://github.com/MystiPanda)
- Improvements to error handling [#449](https://github.com/softprops/action-gh-release/pull/449) via [@till](https://github.com/till)
## 2.0.4
- Minor follow up to [#417](https://github.com/softprops/action-gh-release/pull/417). [#425](https://github.com/softprops/action-gh-release/pull/425)
## 2.0.3
- Declare `make_latest` as an input field in `action.yml` [#419](https://github.com/softprops/action-gh-release/pull/419)
## 2.0.2
- Revisit approach to [#384](https://github.com/softprops/action-gh-release/pull/384) making unresolved pattern failures opt-in [#417](https://github.com/softprops/action-gh-release/pull/417)
## 2.0.1
- Add support for make_latest property [#304](https://github.com/softprops/action-gh-release/pull/304) via [@samueljseay](https://github.com/samueljseay)
- Fail run if files setting contains invalid patterns [#384](https://github.com/softprops/action-gh-release/pull/384) via [@rpdelaney](https://github.com/rpdelaney)
- Add support for proxy env variables (don't use node-fetch) [#386](https://github.com/softprops/action-gh-release/pull/386/) via [@timor-raiman](https://github.com/timor-raiman)
- Suppress confusing warning when input_files is empty [#389](https://github.com/softprops/action-gh-release/pull/389) via [@Drowze](https://github.com/Drowze)
## 2.0.0
- `2.0.0`!? this release corrects a disjunction between git tag versions used in the marketplace and the versions listed in this file. Previous versions should have really been 1.\*. Going forward this should be better aligned.
- Upgrade action.yml declaration to node20 to address deprecations
## 0.1.15
- Upgrade to action.yml declaration to node16 to address deprecations
- Upgrade dependencies
- Add `asset` output as a JSON array containing information about the uploaded assets
## 0.1.14
- provides a 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 substituted 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
@@ -274,7 +9,7 @@ current `master` no longer reproduces the finalize-race behavior reported in `#7
## 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 now resolved first from an env variable and then from an input [#133](https://github.com/softprops/action-gh-release/pull/133)
- 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
@@ -287,14 +22,14 @@ current `master` no longer reproduces the finalize-race behavior reported in `#7
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 where your `files` input does not actually match what you expect [#55](https://github.com/softprops/action-gh-release/pull/55)
- 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 referring to release in workflow steps following the step that uses this action [#60](https://github.com/softprops/action-gh-release/pull/60)
- 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 implemented in the opposite order! [#85](https://github.com/softprops/action-gh-release/pull/85)
- 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 due to either an invalid token or other circumstance causing the api call to fail [#111](https://github.com/softprops/action-gh-release/pull/111)
- 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
@@ -304,7 +39,7 @@ This is a release catch up have a hiatus. Future releases will happen more frequ
- 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)
- Added basic GitHub API retry support to manage API turbulence [#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
@@ -319,7 +54,7 @@ This is now fixed.
- 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 delimited 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
- name: Release
+1 -1
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
a copy of this software and associated documentation files (the
+36 -78
View File
@@ -21,16 +21,6 @@
<br />
- [🤸 Usage](#-usage)
- [🚥 Limit releases to pushes to tags](#-limit-releases-to-pushes-to-tags)
- [⬆️ Uploading release assets](#️-uploading-release-assets)
- [📝 External release notes](#-external-release-notes)
- [💅 Customizing](#-customizing)
- [inputs](#inputs)
- [outputs](#outputs)
- [environment variables](#environment-variables)
- [Permissions](#permissions)
## 🤸 Usage
### 🚥 Limit releases to pushes to tags
@@ -51,10 +41,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v2
- name: Release
uses: softprops/action-gh-release@v2
if: github.ref_type == 'tag'
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
```
You can also use push config tag filter
@@ -72,9 +62,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v2
- name: Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v1
```
### ⬆️ Uploading release assets
@@ -85,7 +75,6 @@ GitHub release and all are optional.
A common case for GitHub releases is to upload your binary after its been validated and packaged.
Use the `with.files` input to declare a newline-delimited list of glob expressions matching the files
you wish to upload to GitHub releases. If you'd like you can just list the files by name directly.
If a tag already has a GitHub release, the existing release will be updated with the release assets.
Below is an example of uploading a single asset named `Release.txt`
@@ -99,14 +88,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v2
- name: Build
run: echo ${{ github.sha }} > Release.txt
- name: Test
run: cat Release.txt
- name: Release
uses: softprops/action-gh-release@v2
if: github.ref_type == 'tag'
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: Release.txt
```
@@ -123,23 +112,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v2
- name: Build
run: echo ${{ github.sha }} > Release.txt
- name: Test
run: cat Release.txt
- name: Release
uses: softprops/action-gh-release@v2
if: github.ref_type == 'tag'
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
Release.txt
LICENSE
```
> **⚠️ Note:** Notice the `|` in the yaml syntax above ☝️. That lets you effectively declare a multi-line yaml string. You can learn more about multi-line yaml syntax [here](https://yaml-multiline.info)
> **⚠️ Note 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 '\\','/'`
> **⚠️ 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)
### 📝 External release notes
@@ -157,18 +144,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v2
- name: Generate Changelog
run: echo "# Good things have arrived" > ${{ github.workspace }}-CHANGELOG.txt
- name: Release
uses: softprops/action-gh-release@v2
if: github.ref_type == 'tag'
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
body_path: ${{ github.workspace }}-CHANGELOG.txt
repository: my_gh_org/my_gh_repo
# 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:
GITHUB_REPOSITORY: my_gh_org/my_gh_repo
```
### 💅 Customizing
@@ -177,25 +165,20 @@ jobs:
The following are optional as `step.with` keys
| Name | Type | Description |
| -------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `body` | String | 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 |
| `prerelease` | Boolean | Indicator of whether or not is a prerelease |
| `preserve_order` | Boolean | Indicator of whether order of files should be preserved when uploading assets |
| `files` | String | Newline-delimited globs of paths to assets to upload for release |
| `overwrite_files` | Boolean | Indicator of whether files should be overwritten when they already exist. Defaults to true |
| `name` | String | Name of the release. defaults to tag name |
| `tag_name` | String | Name of a tag. defaults to `github.ref_name` |
| `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 |
| `make_latest` | String | Specifies whether this release should be set as the latest release for the repository. Drafts and prereleases cannot be set as latest. Can be `true`, `false`, or `legacy`. Uses GitHub api defaults if not provided |
| Name | Type | Description |
| -------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `body` | String | 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 |
| `prerelease` | Boolean | Indicator of whether or not is a prerelease |
| `files` | String | Newline-delimited globs of paths to assets to upload for release |
| `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) |
💡 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.
@@ -208,14 +191,11 @@ release will retain its original info.
The following outputs can be accessed via `${{ steps.<step-id>.outputs }}` from this action
| Name | Type | Description |
| ------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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 updated (newly uploaded or overwritten) 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.
| Name | Type | Description |
| ------------ | ------ | --------------------------------------- |
| `url` | String | Github.com URL for the release |
| `id` | String | Release ID |
| `upload_url` | String | URL for uploading assets to the release |
#### environment variables
@@ -228,26 +208,4 @@ The following `step.env` keys are allowed as a fallback but deprecated in favor
> **⚠️ Note:** This action was previously implemented as a Docker container, limiting its use to GitHub Actions Linux virtual environments only. With recent releases, we now support cross platform usage. You'll need to remove the `docker://` prefix in these versions
### Permissions
This Action requires the following permissions on the GitHub integration token:
```yaml
permissions:
contents: write
```
When used with `discussion_category_name`, additional permission is needed:
```yaml
permissions:
contents: write
discussions: write
```
[GitHub token permissions](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) can be set for an individual job, workflow, or for Actions as a whole.
Note that if you intend to run workflows on the release event (`on: { release: { types: [published] } }`), you need to use
a personal access token for this action, as the [default `secrets.GITHUB_TOKEN` does not trigger another workflow](https://github.com/actions/create-release/issues/71).
Doug Tangren (softprops) 2019
+16 -723
View File
@@ -1,732 +1,25 @@
import {
asset,
findTagFromReleases,
finalizeRelease,
mimeOrDefault,
release,
Release,
Releaser,
upload,
} from '../src/github';
//import * as assert from "assert";
//const assert = require('assert');
import * as assert from "assert";
import { mimeOrDefault, asset } from "../src/github";
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { assert, describe, expect, it, vi } from 'vitest';
describe('github', () => {
const config = {
github_token: 'test-token',
github_ref: 'refs/tags/v1.0.0',
github_repository: 'owner/repo',
input_tag_name: undefined,
input_name: undefined,
input_body: undefined,
input_body_path: undefined,
input_files: [],
input_draft: undefined,
input_prerelease: undefined,
input_preserve_order: undefined,
input_overwrite_files: undefined,
input_fail_on_unmatched_files: false,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
input_append_body: false,
input_make_latest: undefined,
};
describe('mimeOrDefault', () => {
it('returns a specific mime for common path', async () => {
assert.equal(mimeOrDefault('foo.tar.gz'), 'application/gzip');
describe("github", () => {
describe("mimeOrDefault", () => {
it("returns a specific mime for common path", async () => {
assert.equal(mimeOrDefault("foo.tar.gz"), "application/gzip");
});
it('returns default mime for uncommon path', async () => {
assert.equal(mimeOrDefault('foo.uncommon'), 'application/octet-stream');
it("returns default mime for uncommon path", async () => {
assert.equal(mimeOrDefault("foo.uncommon"), "application/octet-stream");
});
});
describe('asset', () => {
it('derives asset info from a path', async () => {
const { name, mime, size } = asset('tests/data/foo/bar.txt');
assert.equal(name, 'bar.txt');
assert.equal(mime, 'text/plain');
describe("asset", () => {
it("derives asset info from a path", async () => {
const { name, mime, size, data } = asset("tests/data/foo/bar.txt");
assert.equal(name, "bar.txt");
assert.equal(mime, "text/plain");
assert.equal(size, 10);
});
});
describe('findTagFromReleases', () => {
const owner = 'owner';
const repo = 'repo';
const mockRelease: Release = {
id: 1,
upload_url: `https://api.github.com/repos/${owner}/${repo}/releases/1/assets`,
html_url: `https://github.com/${owner}/${repo}/releases/tag/v1.0.0`,
tag_name: 'v1.0.0',
name: 'Test Release',
body: 'Test body',
target_commitish: 'main',
draft: false,
prerelease: false,
assets: [],
} as const;
const mockReleaser: Releaser = {
getReleaseByTag: () => Promise.reject({ status: 404 }),
createRelease: () => Promise.reject('Not implemented'),
updateRelease: () => Promise.reject('Not implemented'),
finalizeRelease: () => Promise.reject('Not implemented'),
allReleases: async function* () {
yield { data: [mockRelease] };
},
listReleaseAssets: () => Promise.reject('Not implemented'),
deleteReleaseAsset: () => Promise.reject('Not implemented'),
deleteRelease: () => Promise.reject('Not implemented'),
updateReleaseAsset: () => Promise.reject('Not implemented'),
uploadReleaseAsset: () => Promise.reject('Not implemented'),
} as const;
it('finds a release by tag using direct API lookup', async () => {
const targetTag = 'v1.0.0';
const targetRelease = {
...mockRelease,
tag_name: targetTag,
};
const releaser = {
...mockReleaser,
getReleaseByTag: () => Promise.resolve({ data: targetRelease }),
};
const result = await findTagFromReleases(releaser, owner, repo, targetTag);
assert.deepStrictEqual(result, targetRelease);
});
it('returns undefined when release is not found (404)', async () => {
const releaser = {
...mockReleaser,
getReleaseByTag: () => Promise.reject({ status: 404 }),
};
const result = await findTagFromReleases(releaser, owner, repo, 'nonexistent');
assert.strictEqual(result, undefined);
});
it('re-throws non-404 errors', async () => {
const releaser = {
...mockReleaser,
getReleaseByTag: () => Promise.reject({ status: 500, message: 'Server error' }),
};
try {
await findTagFromReleases(releaser, owner, repo, 'v1.0.0');
assert.fail('Expected an error to be thrown');
} catch (error) {
assert.strictEqual(error.status, 500);
}
});
it('finds a release with empty tag name', async () => {
const emptyTag = '';
const targetRelease = {
...mockRelease,
tag_name: emptyTag,
};
const releaser = {
...mockReleaser,
getReleaseByTag: () => Promise.resolve({ data: targetRelease }),
};
const result = await findTagFromReleases(releaser, owner, repo, emptyTag);
assert.deepStrictEqual(result, targetRelease);
});
});
describe('finalizeRelease input_draft behavior', () => {
const draftRelease: Release = {
id: 1,
upload_url: 'test',
html_url: 'test',
tag_name: 'v1.0.0',
name: 'test',
body: 'test',
target_commitish: 'main',
draft: true,
prerelease: false,
assets: [],
};
const finalizedRelease: Release = {
...draftRelease,
draft: false,
};
const publishedPrerelease: Release = {
...draftRelease,
draft: false,
prerelease: true,
};
it.each([
{
name: 'returns early when input_draft is true',
input_draft: true,
release: draftRelease,
expectedCalls: 0,
expectedResult: draftRelease,
},
{
name: 'finalizes release when input_draft is false',
input_draft: false,
release: draftRelease,
expectedCalls: 1,
expectedResult: finalizedRelease,
},
{
name: 'returns early when release is already published',
input_draft: false,
release: publishedPrerelease,
expectedCalls: 0,
expectedResult: publishedPrerelease,
},
])('$name', async ({ input_draft, release, expectedCalls, expectedResult }) => {
const finalizeReleaseSpy = vi.fn(async () => ({ data: finalizedRelease }));
const releaser: Releaser = {
getReleaseByTag: () => Promise.reject('Not implemented'),
createRelease: () => Promise.reject('Not implemented'),
updateRelease: () => Promise.reject('Not implemented'),
finalizeRelease: finalizeReleaseSpy,
allReleases: async function* () {
throw new Error('Not implemented');
},
listReleaseAssets: () => Promise.reject('Not implemented'),
deleteReleaseAsset: () => Promise.reject('Not implemented'),
deleteRelease: () => Promise.reject('Not implemented'),
updateReleaseAsset: () => Promise.reject('Not implemented'),
uploadReleaseAsset: () => Promise.reject('Not implemented'),
};
const result = await finalizeRelease(
{
...config,
input_draft,
},
releaser,
release,
);
expect(finalizeReleaseSpy).toHaveBeenCalledTimes(expectedCalls);
assert.strictEqual(result, expectedResult);
if (expectedCalls === 1) {
expect(finalizeReleaseSpy).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
release_id: release.id,
});
}
});
it('deletes a newly created draft when tag creation is blocked by repository rules', async () => {
const finalizeReleaseSpy = vi.fn(async () => {
throw {
status: 422,
response: {
data: {
errors: [
{
field: 'pre_receive',
message:
'pre_receive Repository rule violations found\n\nCannot create ref due to creations being restricted.\n\n',
},
],
},
},
};
});
const deleteReleaseSpy = vi.fn(async () => undefined);
const releaser: Releaser = {
getReleaseByTag: () => Promise.reject('Not implemented'),
createRelease: () => Promise.reject('Not implemented'),
updateRelease: () => Promise.reject('Not implemented'),
finalizeRelease: finalizeReleaseSpy,
allReleases: async function* () {
throw new Error('Not implemented');
},
listReleaseAssets: () => Promise.reject('Not implemented'),
deleteReleaseAsset: () => Promise.reject('Not implemented'),
deleteRelease: deleteReleaseSpy,
updateReleaseAsset: () => Promise.reject('Not implemented'),
uploadReleaseAsset: () => Promise.reject('Not implemented'),
};
await expect(
finalizeRelease(
{
...config,
input_draft: false,
},
releaser,
draftRelease,
true,
),
).rejects.toThrow(
'Tag creation for v1.0.0 is blocked by repository rules. Deleted draft release 1 to avoid leaving an orphaned draft release.',
);
expect(finalizeReleaseSpy).toHaveBeenCalledTimes(1);
expect(deleteReleaseSpy).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
release_id: draftRelease.id,
});
});
it('does not delete an existing draft release when tag creation is blocked by repository rules', async () => {
const finalizeReleaseSpy = vi.fn(async () => {
throw {
status: 422,
response: {
data: {
errors: [
{
field: 'pre_receive',
message:
'pre_receive Repository rule violations found\n\nCannot create ref due to creations being restricted.\n\n',
},
],
},
},
};
});
const deleteReleaseSpy = vi.fn(async () => undefined);
const releaser: Releaser = {
getReleaseByTag: () => Promise.reject('Not implemented'),
createRelease: () => Promise.reject('Not implemented'),
updateRelease: () => Promise.reject('Not implemented'),
finalizeRelease: finalizeReleaseSpy,
allReleases: async function* () {
throw new Error('Not implemented');
},
listReleaseAssets: () => Promise.reject('Not implemented'),
deleteReleaseAsset: () => Promise.reject('Not implemented'),
deleteRelease: deleteReleaseSpy,
updateReleaseAsset: () => Promise.reject('Not implemented'),
uploadReleaseAsset: () => Promise.reject('Not implemented'),
};
await expect(
finalizeRelease(
{
...config,
input_draft: false,
},
releaser,
draftRelease,
false,
1,
),
).rejects.toThrow('Too many retries.');
expect(finalizeReleaseSpy).toHaveBeenCalledTimes(1);
expect(deleteReleaseSpy).not.toHaveBeenCalled();
});
});
describe('error handling', () => {
it('creates published prereleases without the forced draft-first path', async () => {
const prereleaseConfig = {
...config,
input_prerelease: true,
input_draft: false,
};
const createdRelease: Release = {
id: 1,
upload_url: 'test',
html_url: 'test',
tag_name: 'v1.0.0',
name: 'test',
body: 'test',
target_commitish: 'main',
draft: false,
prerelease: true,
assets: [],
};
const createReleaseSpy = vi.fn(async () => ({ data: createdRelease }));
const mockReleaser: Releaser = {
getReleaseByTag: () => Promise.reject({ status: 404 }),
createRelease: createReleaseSpy,
updateRelease: () => Promise.reject('Not implemented'),
finalizeRelease: () => Promise.reject('Not implemented'),
allReleases: async function* () {
yield { data: [createdRelease] };
},
listReleaseAssets: () => Promise.reject('Not implemented'),
deleteReleaseAsset: () => Promise.reject('Not implemented'),
deleteRelease: () => Promise.reject('Not implemented'),
updateReleaseAsset: () => Promise.reject('Not implemented'),
uploadReleaseAsset: () => Promise.reject('Not implemented'),
} as const;
const result = await release(prereleaseConfig, mockReleaser, 1);
assert.equal(result.release.id, createdRelease.id);
assert.equal(result.created, true);
expect(createReleaseSpy).toHaveBeenCalledWith(
expect.objectContaining({
draft: false,
prerelease: true,
}),
);
});
it('retries upload after deleting conflicting asset on 422 already_exists race', async () => {
const uploadReleaseAsset = vi
.fn()
.mockRejectedValueOnce({
status: 422,
response: { data: { errors: [{ code: 'already_exists' }] } },
})
.mockResolvedValueOnce({
status: 201,
data: { id: 123, name: 'release.txt' },
});
const listReleaseAssets = vi.fn().mockResolvedValue([{ id: 99, name: 'release.txt' }]);
const deleteReleaseAsset = vi.fn().mockResolvedValue(undefined);
const mockReleaser: Releaser = {
getReleaseByTag: () => Promise.reject('Not implemented'),
createRelease: () => Promise.reject('Not implemented'),
updateRelease: () => Promise.reject('Not implemented'),
finalizeRelease: () => Promise.reject('Not implemented'),
allReleases: async function* () {
throw new Error('Not implemented');
},
listReleaseAssets,
deleteReleaseAsset,
uploadReleaseAsset,
};
const result = await upload(
config,
mockReleaser,
'https://uploads.github.com/repos/owner/repo/releases/1/assets',
'__tests__/release.txt',
[],
);
expect(result).toStrictEqual({ id: 123, name: 'release.txt' });
expect(listReleaseAssets).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
release_id: 1,
});
expect(deleteReleaseAsset).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
asset_id: 99,
});
expect(uploadReleaseAsset).toHaveBeenCalledTimes(2);
});
it('handles 422 already_exists error gracefully', async () => {
const existingRelease = {
id: 1,
upload_url: 'test',
html_url: 'test',
tag_name: 'v1.0.0',
name: 'test',
body: 'test',
target_commitish: 'main',
draft: false,
prerelease: false,
assets: [],
};
let createAttempts = 0;
const mockReleaser: Releaser = {
getReleaseByTag: ({ tag }) => {
// First call returns 404 (release doesn't exist yet), subsequent calls find it
if (createAttempts === 0) {
return Promise.reject({ status: 404 });
}
return Promise.resolve({ data: existingRelease });
},
createRelease: () => {
createAttempts++;
return Promise.reject({
status: 422,
response: { data: { errors: [{ code: 'already_exists' }] } },
});
},
updateRelease: () =>
Promise.resolve({
data: {
id: 1,
upload_url: 'test',
html_url: 'test',
tag_name: 'v1.0.0',
name: 'test',
body: 'test',
target_commitish: 'main',
draft: true,
prerelease: false,
assets: [],
},
}),
finalizeRelease: () => Promise.reject('Not implemented'),
allReleases: async function* () {
yield { data: [existingRelease] };
},
listReleaseAssets: () => Promise.reject('Not implemented'),
deleteReleaseAsset: () => Promise.reject('Not implemented'),
deleteRelease: () => Promise.reject('Not implemented'),
uploadReleaseAsset: () => Promise.reject('Not implemented'),
} as const;
const result = await release(config, mockReleaser, 2);
assert.ok(result);
assert.equal(result.release.id, 1);
assert.equal(result.created, false);
});
it('reuses a canonical release after concurrent create success and removes empty duplicates', async () => {
const canonicalRelease: Release = {
id: 1,
upload_url: 'canonical-upload',
html_url: 'canonical-html',
tag_name: 'v1.0.0',
name: 'canonical',
body: 'test',
target_commitish: 'main',
draft: true,
prerelease: false,
assets: [],
};
const duplicateRelease: Release = {
id: 2,
upload_url: 'duplicate-upload',
html_url: 'duplicate-html',
tag_name: 'v1.0.0',
name: 'duplicate',
body: 'test',
target_commitish: 'main',
draft: true,
prerelease: false,
assets: [],
};
let lookupCount = 0;
const deleteReleaseSpy = vi.fn(async () => undefined);
const mockReleaser: Releaser = {
getReleaseByTag: () => {
lookupCount += 1;
if (lookupCount === 1) {
return Promise.reject({ status: 404 });
}
return Promise.resolve({ data: canonicalRelease });
},
createRelease: () => Promise.resolve({ data: duplicateRelease }),
updateRelease: () => Promise.reject('Not implemented'),
finalizeRelease: () => Promise.reject('Not implemented'),
allReleases: async function* () {
yield { data: [duplicateRelease, canonicalRelease] };
},
listReleaseAssets: () => Promise.reject('Not implemented'),
deleteReleaseAsset: () => Promise.reject('Not implemented'),
deleteRelease: deleteReleaseSpy,
updateReleaseAsset: () => Promise.reject('Not implemented'),
uploadReleaseAsset: () => Promise.reject('Not implemented'),
};
const result = await release(config, mockReleaser, 2);
assert.equal(result.release.id, canonicalRelease.id);
assert.equal(result.created, false);
expect(deleteReleaseSpy).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
release_id: duplicateRelease.id,
});
});
it('falls back to recent releases when tag lookup still lags after create', async () => {
const canonicalRelease: Release = {
id: 1,
upload_url: 'canonical-upload',
html_url: 'canonical-html',
tag_name: 'v1.0.0',
name: 'canonical',
body: 'test',
target_commitish: 'main',
draft: true,
prerelease: false,
assets: [],
};
const duplicateRelease: Release = {
id: 2,
upload_url: 'duplicate-upload',
html_url: 'duplicate-html',
tag_name: 'v1.0.0',
name: 'duplicate',
body: 'test',
target_commitish: 'main',
draft: true,
prerelease: false,
assets: [],
};
const deleteReleaseSpy = vi.fn(async () => undefined);
const mockReleaser: Releaser = {
getReleaseByTag: () => Promise.reject({ status: 404 }),
createRelease: () => Promise.resolve({ data: duplicateRelease }),
updateRelease: () => Promise.reject('Not implemented'),
finalizeRelease: () => Promise.reject('Not implemented'),
allReleases: async function* () {
yield { data: [duplicateRelease, canonicalRelease] };
},
listReleaseAssets: () => Promise.reject('Not implemented'),
deleteReleaseAsset: () => Promise.reject('Not implemented'),
deleteRelease: deleteReleaseSpy,
updateReleaseAsset: () => Promise.reject('Not implemented'),
uploadReleaseAsset: () => Promise.reject('Not implemented'),
};
const result = await release(config, mockReleaser, 1);
assert.equal(result.release.id, canonicalRelease.id);
assert.equal(result.created, false);
expect(deleteReleaseSpy).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
release_id: duplicateRelease.id,
});
});
});
describe('upload', () => {
it('restores a dotfile label when GitHub normalizes the uploaded asset name', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'gh-release-dotfile-'));
const dotfilePath = join(tempDir, '.config');
writeFileSync(dotfilePath, 'config');
const updateReleaseAssetSpy = vi.fn(async () => ({
data: {
id: 1,
name: 'default.config',
label: '.config',
},
}));
const releaser: Releaser = {
getReleaseByTag: () => Promise.reject('Not implemented'),
createRelease: () => Promise.reject('Not implemented'),
updateRelease: () => Promise.reject('Not implemented'),
finalizeRelease: () => Promise.reject('Not implemented'),
allReleases: async function* () {
throw new Error('Not implemented');
},
listReleaseAssets: () => Promise.reject('Not implemented'),
deleteReleaseAsset: () => Promise.reject('Not implemented'),
deleteRelease: () => Promise.reject('Not implemented'),
updateReleaseAsset: updateReleaseAssetSpy,
uploadReleaseAsset: () =>
Promise.resolve({
status: 201,
data: {
id: 1,
name: 'default.config',
label: '',
},
}),
};
try {
const result = await upload(
config,
releaser,
'https://uploads.example.test/assets',
dotfilePath,
[],
);
expect(updateReleaseAssetSpy).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
asset_id: 1,
name: 'default.config',
label: '.config',
});
expect(result).toEqual({
id: 1,
name: 'default.config',
label: '.config',
});
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it('matches an existing asset by label when overwriting a dotfile', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'gh-release-dotfile-'));
const dotfilePath = join(tempDir, '.config');
writeFileSync(dotfilePath, 'config');
const deleteReleaseAssetSpy = vi.fn(async () => undefined);
const releaser: Releaser = {
getReleaseByTag: () => Promise.reject('Not implemented'),
createRelease: () => Promise.reject('Not implemented'),
updateRelease: () => Promise.reject('Not implemented'),
finalizeRelease: () => Promise.reject('Not implemented'),
allReleases: async function* () {
throw new Error('Not implemented');
},
listReleaseAssets: () => Promise.reject('Not implemented'),
deleteReleaseAsset: deleteReleaseAssetSpy,
deleteRelease: () => Promise.reject('Not implemented'),
updateReleaseAsset: () =>
Promise.resolve({
data: {
id: 2,
name: 'default.config',
label: '.config',
},
}),
uploadReleaseAsset: () =>
Promise.resolve({
status: 201,
data: {
id: 2,
name: 'default.config',
label: '',
},
}),
};
try {
await upload(config, releaser, 'https://uploads.example.test/assets', dotfilePath, [
{
id: 1,
name: 'default.config',
label: '.config',
},
]);
expect(deleteReleaseAssetSpy).toHaveBeenCalledWith({
asset_id: 1,
owner: 'owner',
repo: 'repo',
});
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
assert.equal(data.toString(), "release me");
});
});
});
+170 -437
View File
@@ -1,525 +1,258 @@
import {
alignAssetName,
releaseBody,
isTag,
paths,
parseConfig,
parseInputFiles,
paths,
releaseBody,
unmatchedPatterns,
uploadUrl,
} from '../src/util';
uploadUrl
} from "../src/util";
import * as assert from "assert";
import { assert, describe, expect, it } from 'vitest';
describe('util', () => {
describe('uploadUrl', () => {
it('strips template', () => {
describe("util", () => {
describe("uploadUrl", () => {
it("stripts 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{?name,label}"
),
'https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets',
"https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets"
);
});
});
describe('parseInputFiles', () => {
it('parses empty strings', () => {
assert.deepStrictEqual(parseInputFiles(''), []);
describe("parseInputFiles", () => {
it("parses empty strings", () => {
assert.deepStrictEqual(parseInputFiles(""), []);
});
it('parses comma-delimited strings', () => {
assert.deepStrictEqual(parseInputFiles('foo,bar'), ['foo', 'bar']);
it("parses comma-delimited strings", () => {
assert.deepStrictEqual(parseInputFiles("foo,bar"), ["foo", "bar"]);
});
it('parses newline and comma-delimited (and then some)', () => {
assert.deepStrictEqual(parseInputFiles('foo,bar\nbaz,boom,\n\ndoom,loom '), [
'foo',
'bar',
'baz',
'boom',
'doom',
'loom',
]);
});
it('handles globs with brace groups containing commas', () => {
assert.deepStrictEqual(parseInputFiles('./**/*.{exe,deb,tar.gz}\nfoo,bar'), [
'./**/*.{exe,deb,tar.gz}',
'foo',
'bar',
]);
});
it('handles single-line brace pattern correctly', () => {
assert.deepStrictEqual(parseInputFiles('./**/*.{exe,deb,tar.gz}'), [
'./**/*.{exe,deb,tar.gz}',
]);
});
});
describe('releaseBody', () => {
it('uses input body', () => {
assert.equal(
'foo',
releaseBody({
github_ref: '',
github_repository: '',
github_token: '',
input_body: 'foo',
input_body_path: undefined,
input_draft: false,
input_prerelease: false,
input_preserve_order: undefined,
input_files: [],
input_overwrite_files: undefined,
input_name: undefined,
input_tag_name: undefined,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
input_make_latest: undefined,
}),
);
});
it('uses input body path', () => {
assert.equal(
'bar',
releaseBody({
github_ref: '',
github_repository: '',
github_token: '',
input_body: undefined,
input_body_path: '__tests__/release.txt',
input_draft: false,
input_prerelease: false,
input_preserve_order: undefined,
input_files: [],
input_overwrite_files: undefined,
input_name: undefined,
input_tag_name: undefined,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
input_make_latest: undefined,
}),
);
});
it('defaults to body path when both body and body path are provided', () => {
assert.equal(
'bar',
releaseBody({
github_ref: '',
github_repository: '',
github_token: '',
input_body: 'foo',
input_body_path: '__tests__/release.txt',
input_draft: false,
input_prerelease: false,
input_preserve_order: undefined,
input_files: [],
input_overwrite_files: undefined,
input_name: undefined,
input_tag_name: undefined,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
input_make_latest: undefined,
}),
);
});
it('falls back to body when body_path is missing', () => {
assert.equal(
releaseBody({
github_ref: '',
github_repository: '',
github_token: '',
input_body: 'fallback-body',
input_body_path: '__tests__/does-not-exist.txt',
input_draft: false,
input_prerelease: false,
input_files: [],
input_overwrite_files: undefined,
input_preserve_order: undefined,
input_name: undefined,
input_tag_name: undefined,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
input_make_latest: undefined,
}),
'fallback-body',
);
});
it('returns undefined when body_path is missing and body is not provided', () => {
assert.equal(
releaseBody({
github_ref: '',
github_repository: '',
github_token: '',
input_body: undefined,
input_body_path: '__tests__/does-not-exist.txt',
input_draft: false,
input_prerelease: false,
input_files: [],
input_overwrite_files: undefined,
input_preserve_order: undefined,
input_name: undefined,
input_tag_name: undefined,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
input_make_latest: undefined,
}),
undefined,
it("parses newline and comma-delimited (and then some)", () => {
assert.deepStrictEqual(
parseInputFiles("foo,bar\nbaz,boom,\n\ndoom,loom "),
["foo", "bar", "baz", "boom", "doom", "loom"]
);
});
});
describe('parseConfig', () => {
it('parses basic config', () => {
describe("releaseBody", () => {
it("uses input body", () => {
assert.equal(
"foo",
releaseBody({
github_ref: "",
github_repository: "",
github_token: "",
input_body: "foo",
input_body_path: undefined,
input_draft: false,
input_prerelease: false,
input_files: [],
input_name: undefined,
input_tag_name: undefined,
input_target_commitish: undefined,
input_discussion_category_name: undefined
})
);
});
it("uses input body path", () => {
assert.equal(
"bar",
releaseBody({
github_ref: "",
github_repository: "",
github_token: "",
input_body: undefined,
input_body_path: "__tests__/release.txt",
input_draft: false,
input_prerelease: false,
input_files: [],
input_name: undefined,
input_tag_name: undefined,
input_target_commitish: undefined,
input_discussion_category_name: undefined
})
);
});
it("defaults to body path when both body and body path are provided", () => {
assert.equal(
"bar",
releaseBody({
github_ref: "",
github_repository: "",
github_token: "",
input_body: "foo",
input_body_path: "__tests__/release.txt",
input_draft: false,
input_prerelease: false,
input_files: [],
input_name: undefined,
input_tag_name: undefined,
input_target_commitish: undefined,
input_discussion_category_name: undefined
})
);
});
});
describe("parseConfig", () => {
it("parses basic config", () => {
assert.deepStrictEqual(parseConfig({}), {
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
});
});
it("parses basic config with commitish", () => {
assert.deepStrictEqual(
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: '',
INPUT_TARGET_COMMITISH: "affa18ef97bc9db20076945705aba8c516139abd"
}),
{
github_ref: '',
github_repository: '',
github_token: '',
input_working_directory: undefined,
input_append_body: false,
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
}
);
});
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_preserve_order: undefined,
input_files: [],
input_overwrite_files: 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,
input_make_latest: undefined,
},
input_discussion_category_name: "releases"
}
);
});
it('parses basic config with commitish', () => {
it("prefers GITHUB_TOKEN over token input for backwards compatibility", () => {
assert.deepStrictEqual(
parseConfig({
INPUT_TARGET_COMMITISH: 'affa18ef97bc9db20076945705aba8c516139abd',
INPUT_DRAFT: "false",
INPUT_PRERELEASE: "true",
GITHUB_TOKEN: "env-token",
INPUT_TOKEN: "input-token"
}),
{
github_ref: '',
github_repository: '',
github_token: '',
input_working_directory: undefined,
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
input_draft: undefined,
input_prerelease: undefined,
input_files: [],
input_overwrite_files: undefined,
input_preserve_order: undefined,
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,
input_make_latest: undefined,
},
);
});
it('supports discussion category names', () => {
assert.deepStrictEqual(
parseConfig({
INPUT_DISCUSSION_CATEGORY_NAME: 'releases',
}),
{
github_ref: '',
github_repository: '',
github_token: '',
input_working_directory: undefined,
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
input_draft: undefined,
input_prerelease: undefined,
input_files: [],
input_preserve_order: undefined,
input_name: undefined,
input_overwrite_files: 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,
input_make_latest: undefined,
},
);
});
it('supports generating release notes', () => {
assert.deepStrictEqual(
parseConfig({
INPUT_GENERATE_RELEASE_NOTES: 'true',
}),
{
github_ref: '',
github_repository: '',
github_token: '',
input_working_directory: undefined,
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
input_draft: undefined,
input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [],
input_overwrite_files: 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: true,
input_make_latest: undefined,
},
);
});
it('prefers GITHUB_TOKEN over token input for backwards compatibility', () => {
assert.deepStrictEqual(
parseConfig({
INPUT_DRAFT: 'false',
INPUT_PRERELEASE: 'true',
INPUT_PRESERVE_ORDER: 'true',
GITHUB_TOKEN: 'env-token',
INPUT_TOKEN: 'input-token',
}),
{
github_ref: '',
github_repository: '',
github_token: 'env-token',
input_working_directory: undefined,
input_append_body: false,
github_ref: "",
github_repository: "",
github_token: "env-token",
input_body: undefined,
input_body_path: undefined,
input_draft: false,
input_prerelease: true,
input_preserve_order: true,
input_files: [],
input_overwrite_files: 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,
input_make_latest: undefined,
},
input_discussion_category_name: undefined
}
);
});
it('uses input token as the source of GITHUB_TOKEN by default', () => {
it("uses input token as the source of GITHUB_TOKEN by default", () => {
assert.deepStrictEqual(
parseConfig({
INPUT_DRAFT: 'false',
INPUT_PRERELEASE: 'true',
INPUT_TOKEN: 'input-token',
INPUT_DRAFT: "false",
INPUT_PRERELEASE: "true",
INPUT_TOKEN: "input-token"
}),
{
github_ref: '',
github_repository: '',
github_token: 'input-token',
input_working_directory: undefined,
input_append_body: false,
github_ref: "",
github_repository: "",
github_token: "input-token",
input_body: undefined,
input_body_path: undefined,
input_draft: false,
input_prerelease: true,
input_preserve_order: undefined,
input_files: [],
input_overwrite_files: 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,
input_make_latest: undefined,
},
input_discussion_category_name: undefined
}
);
});
it('parses basic config with draft and prerelease', () => {
it("parses basic config with draft and prerelease", () => {
assert.deepStrictEqual(
parseConfig({
INPUT_DRAFT: 'false',
INPUT_PRERELEASE: 'true',
INPUT_DRAFT: "false",
INPUT_PRERELEASE: "true"
}),
{
github_ref: '',
github_repository: '',
github_token: '',
input_working_directory: undefined,
input_append_body: false,
github_ref: "",
github_repository: "",
github_token: "",
input_body: undefined,
input_body_path: undefined,
input_draft: false,
input_prerelease: true,
input_preserve_order: undefined,
input_files: [],
input_overwrite_files: 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,
input_make_latest: undefined,
},
input_discussion_category_name: undefined
}
);
});
it('parses basic config where make_latest is passed', () => {
});
describe("isTag", () => {
it("returns true for tags", async () => {
assert.equal(isTag("refs/tags/foo"), true);
});
it("returns false for other kinds of refs", async () => {
assert.equal(isTag("refs/heads/master"), false);
});
});
describe("paths", () => {
it("resolves files given a set of paths", async () => {
assert.deepStrictEqual(
parseConfig({
INPUT_MAKE_LATEST: 'false',
}),
{
github_ref: '',
github_repository: '',
github_token: '',
input_working_directory: undefined,
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
input_draft: undefined,
input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [],
input_name: undefined,
input_overwrite_files: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
input_make_latest: 'false',
},
);
});
it('parses basic config with append_body', () => {
assert.deepStrictEqual(
parseConfig({
INPUT_APPEND_BODY: 'true',
}),
{
github_ref: '',
github_repository: '',
github_token: '',
input_working_directory: undefined,
input_append_body: true,
input_body: undefined,
input_body_path: undefined,
input_draft: undefined,
input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [],
input_overwrite_files: 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,
input_make_latest: undefined,
},
paths(["tests/data/**/*", "tests/data/does/not/exist/*"]),
["tests/data/foo/bar.txt"]
);
});
});
describe('isTag', () => {
it('returns true for tags', async () => {
assert.equal(isTag('refs/tags/foo'), true);
});
it('returns false for other kinds of refs', async () => {
assert.equal(isTag('refs/heads/master'), false);
});
});
describe('paths', () => {
it('resolves files given a set of paths', async () => {
assert.deepStrictEqual(paths(['tests/data/**/*', 'tests/data/does/not/exist/*']), [
'tests/data/foo/bar.txt',
]);
});
it('resolves files relative to working_directory', async () => {
assert.deepStrictEqual(paths(['data/**/*'], 'tests'), ['tests/data/foo/bar.txt']);
});
});
describe('unmatchedPatterns', () => {
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/*'],
unmatchedPatterns(["tests/data/**/*", "tests/data/does/not/exist/*"]),
["tests/data/does/not/exist/*"]
);
});
it('resolves unmatched relative to working_directory', async () => {
assert.deepStrictEqual(unmatchedPatterns(['data/does/not/exist/*'], 'tests'), [
'data/does/not/exist/*',
]);
});
});
describe('replaceSpacesWithDots', () => {
it('replaces all spaces with dots', () => {
expect(alignAssetName('John Doe.bla')).toBe('John.Doe.bla');
});
it('handles names with multiple spaces', () => {
expect(alignAssetName('John William Doe.bla')).toBe('John.William.Doe.bla');
});
it('returns the same string if there are no spaces', () => {
expect(alignAssetName('JohnDoe')).toBe('JohnDoe');
});
});
});
describe('parseInputFiles edge cases', () => {
it('handles multiple brace groups on same line', () => {
assert.deepStrictEqual(parseInputFiles('./**/*.{exe,deb},./dist/**/*.{zip,tar.gz}'), [
'./**/*.{exe,deb}',
'./dist/**/*.{zip,tar.gz}',
]);
});
it('handles nested braces', () => {
assert.deepStrictEqual(parseInputFiles('path/{a,{b,c}}/file.txt'), ['path/{a,{b,c}}/file.txt']);
});
it('handles empty comma-separated values', () => {
assert.deepStrictEqual(parseInputFiles('foo,,bar'), ['foo', 'bar']);
});
it('handles commas with spaces around braces', () => {
assert.deepStrictEqual(parseInputFiles(' ./**/*.{exe,deb} , file.txt '), [
'./**/*.{exe,deb}',
'file.txt',
]);
});
it('handles mixed newlines and commas with braces', () => {
assert.deepStrictEqual(parseInputFiles('file1.txt\n./**/*.{exe,deb},file2.txt\nfile3.txt'), [
'file1.txt',
'./**/*.{exe,deb}',
'file2.txt',
'file3.txt',
]);
});
});
+3 -24
View File
@@ -13,7 +13,7 @@ inputs:
description: "Gives the release a custom name. Defaults to tag name"
required: false
tag_name:
description: "Gives a tag name. Defaults to github.ref_name"
description: "Gives a tag name. Defaults to github.GITHUB_REF"
required: false
draft:
description: "Creates a draft release. Defaults to false"
@@ -21,19 +21,9 @@ inputs:
prerelease:
description: "Identify the release as a prerelease. Defaults to false"
required: false
preserve_order:
description: "Preserver the order of the artifacts when uploading"
required: false
files:
description: "Newline-delimited list of path globs for asset files to upload"
required: false
working_directory:
description: "Base directory to resolve 'files' globs against (defaults to job working-directory)"
required: false
overwrite_files:
description: "Overwrite existing files with the same name. Defaults to true"
required: false
default: 'true'
fail_on_unmatched_files:
description: "Fails if any of the `files` globs match nothing. Defaults to false"
required: false
@@ -50,17 +40,8 @@ inputs:
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
make_latest:
description: "Specifies whether this release should be set as the latest release for the repository. Drafts and prereleases cannot be set as latest. Can be `true`, `false`, or `legacy`. Uses GitHub api default if not provided"
required: false
env:
GITHUB_TOKEN: "As provided by Github Actions"
"GITHUB_TOKEN": "As provided by Github Actions"
outputs:
url:
description: "URL to the Release HTML Page"
@@ -68,10 +49,8 @@ outputs:
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:
using: "node20"
using: "node12"
main: "dist/index.js"
branding:
color: "green"
+1 -90
View File
@@ -1,90 +1 @@
"use strict";var Rw=Object.create;var pp=Object.defineProperty;var Tw=Object.getOwnPropertyDescriptor;var Fw=Object.getOwnPropertyNames;var Sw=Object.getPrototypeOf,Uw=Object.prototype.hasOwnProperty;var Q=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Nw=(e,t,s,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of Fw(t))!Uw.call(e,i)&&i!==s&&pp(e,i,{get:()=>t[i],enumerable:!(r=Tw(t,i))||r.enumerable});return e};var de=(e,t,s)=>(s=e!=null?Rw(Sw(e)):{},Nw(t||!e||!e.__esModule?pp(s,"default",{value:e,enumerable:!0}):s,e));var Ip=Q(Zs=>{"use strict";var V_=require("net"),_w=require("tls"),$a=require("http"),Qp=require("https"),Yw=require("events"),q_=require("assert"),Ow=require("util");Zs.httpOverHttp=Jw;Zs.httpsOverHttp=Pw;Zs.httpOverHttps=Hw;Zs.httpsOverHttps=Vw;function Jw(e){var t=new Rt(e);return t.request=$a.request,t}function Pw(e){var t=new Rt(e);return t.request=$a.request,t.createSocket=Bp,t.defaultPort=443,t}function Hw(e){var t=new Rt(e);return t.request=Qp.request,t}function Vw(e){var t=new Rt(e);return t.request=Qp.request,t.createSocket=Bp,t.defaultPort=443,t}function Rt(e){var t=this;t.options=e||{},t.proxyOptions=t.options.proxy||{},t.maxSockets=t.options.maxSockets||$a.Agent.defaultMaxSockets,t.requests=[],t.sockets=[],t.on("free",function(r,i,o,n){for(var a=Cp(i,o,n),A=0,c=t.requests.length;A<c;++A){var u=t.requests[A];if(u.host===a.host&&u.port===a.port){t.requests.splice(A,1),u.request.onSocket(r);return}}r.destroy(),t.removeSocket(r)})}Ow.inherits(Rt,Yw.EventEmitter);Rt.prototype.addRequest=function(t,s,r,i){var o=this,n=eA({request:t},o.options,Cp(s,r,i));if(o.sockets.length>=this.maxSockets){o.requests.push(n);return}o.createSocket(n,function(a){a.on("free",A),a.on("close",c),a.on("agentRemove",c),t.onSocket(a);function A(){o.emit("free",a,n)}function c(u){o.removeSocket(a),a.removeListener("free",A),a.removeListener("close",c),a.removeListener("agentRemove",c)}})};Rt.prototype.createSocket=function(t,s){var r=this,i={};r.sockets.push(i);var o=eA({},r.proxyOptions,{method:"CONNECT",path:t.host+":"+t.port,agent:!1,headers:{host:t.host+":"+t.port}});t.localAddress&&(o.localAddress=t.localAddress),o.proxyAuth&&(o.headers=o.headers||{},o.headers["Proxy-Authorization"]="Basic "+new Buffer(o.proxyAuth).toString("base64")),Zt("making CONNECT request");var n=r.request(o);n.useChunkedEncodingByDefault=!1,n.once("response",a),n.once("upgrade",A),n.once("connect",c),n.once("error",u),n.end();function a(l){l.upgrade=!0}function A(l,p,g){process.nextTick(function(){c(l,p,g)})}function c(l,p,g){if(n.removeAllListeners(),p.removeAllListeners(),l.statusCode!==200){Zt("tunneling socket could not be established, statusCode=%d",l.statusCode),p.destroy();var h=new Error("tunneling socket could not be established, statusCode="+l.statusCode);h.code="ECONNRESET",t.request.emit("error",h),r.removeSocket(i);return}if(g.length>0){Zt("got illegal response body from proxy"),p.destroy();var h=new Error("got illegal response body from proxy");h.code="ECONNRESET",t.request.emit("error",h),r.removeSocket(i);return}return Zt("tunneling connection has established"),r.sockets[r.sockets.indexOf(i)]=p,s(p)}function u(l){n.removeAllListeners(),Zt(`tunneling socket could not be established, cause=%s
`,l.message,l.stack);var p=new Error("tunneling socket could not be established, cause="+l.message);p.code="ECONNRESET",t.request.emit("error",p),r.removeSocket(i)}};Rt.prototype.removeSocket=function(t){var s=this.sockets.indexOf(t);if(s!==-1){this.sockets.splice(s,1);var r=this.requests.shift();r&&this.createSocket(r,function(i){r.request.onSocket(i)})}};function Bp(e,t){var s=this;Rt.prototype.createSocket.call(s,e,function(r){var i=e.request.getHeader("host"),o=eA({},s.options,{socket:r,servername:i?i.replace(/:.*$/,""):e.host}),n=_w.connect(0,o);s.sockets[s.sockets.indexOf(r)]=n,t(n)})}function Cp(e,t,s){return typeof e=="string"?{host:e,port:t,localAddress:s}:e}function eA(e){for(var t=1,s=arguments.length;t<s;++t){var r=arguments[t];if(typeof r=="object")for(var i=Object.keys(r),o=0,n=i.length;o<n;++o){var a=i[o];r[a]!==void 0&&(e[a]=r[a])}}return e}var Zt;process.env.NODE_DEBUG&&/\btunnel\b/.test(process.env.NODE_DEBUG)?Zt=function(){var e=Array.prototype.slice.call(arguments);typeof e[0]=="string"?e[0]="TUNNEL: "+e[0]:e.unshift("TUNNEL:"),console.error.apply(console,e)}:Zt=function(){};Zs.debug=Zt});var tA=Q((j_,wp)=>{wp.exports=Ip()});var j=Q((z_,bp)=>{bp.exports={kClose:Symbol("close"),kDestroy:Symbol("destroy"),kDispatch:Symbol("dispatch"),kUrl:Symbol("url"),kWriting:Symbol("writing"),kResuming:Symbol("resuming"),kQueue:Symbol("queue"),kConnect:Symbol("connect"),kConnecting:Symbol("connecting"),kKeepAliveDefaultTimeout:Symbol("default keep alive timeout"),kKeepAliveMaxTimeout:Symbol("max keep alive timeout"),kKeepAliveTimeoutThreshold:Symbol("keep alive timeout threshold"),kKeepAliveTimeoutValue:Symbol("keep alive timeout"),kKeepAlive:Symbol("keep alive"),kHeadersTimeout:Symbol("headers timeout"),kBodyTimeout:Symbol("body timeout"),kServerName:Symbol("server name"),kLocalAddress:Symbol("local address"),kHost:Symbol("host"),kNoRef:Symbol("no ref"),kBodyUsed:Symbol("used"),kBody:Symbol("abstracted request body"),kRunning:Symbol("running"),kBlocking:Symbol("blocking"),kPending:Symbol("pending"),kSize:Symbol("size"),kBusy:Symbol("busy"),kQueued:Symbol("queued"),kFree:Symbol("free"),kConnected:Symbol("connected"),kClosed:Symbol("closed"),kNeedDrain:Symbol("need drain"),kReset:Symbol("reset"),kDestroyed:Symbol.for("nodejs.stream.destroyed"),kResume:Symbol("resume"),kOnError:Symbol("on error"),kMaxHeadersSize:Symbol("max headers size"),kRunningIdx:Symbol("running index"),kPendingIdx:Symbol("pending index"),kError:Symbol("error"),kClients:Symbol("clients"),kClient:Symbol("client"),kParser:Symbol("parser"),kOnDestroyed:Symbol("destroy callbacks"),kPipelining:Symbol("pipelining"),kSocket:Symbol("socket"),kHostHeader:Symbol("host header"),kConnector:Symbol("connector"),kStrictContentLength:Symbol("strict content length"),kMaxRedirections:Symbol("maxRedirections"),kMaxRequests:Symbol("maxRequestsPerClient"),kProxy:Symbol("proxy agent options"),kCounter:Symbol("socket request counter"),kInterceptors:Symbol("dispatch interceptors"),kMaxResponseSize:Symbol("max response size"),kHTTP2Session:Symbol("http2Session"),kHTTP2SessionState:Symbol("http2Session state"),kRetryHandlerDefaultRetry:Symbol("retry agent default retry"),kConstruct:Symbol("constructable"),kListeners:Symbol("listeners"),kHTTPContext:Symbol("http context"),kMaxConcurrentStreams:Symbol("max concurrent streams"),kNoProxyAgent:Symbol("no proxy agent"),kHttpProxyAgent:Symbol("http proxy agent"),kHttpsProxyAgent:Symbol("https proxy agent")}});var L=Q((Z_,zp)=>{"use strict";var yp=Symbol.for("undici.error.UND_ERR"),z=class extends Error{constructor(t){super(t),this.name="UndiciError",this.code="UND_ERR"}static[Symbol.hasInstance](t){return t&&t[yp]===!0}[yp]=!0},xp=Symbol.for("undici.error.UND_ERR_CONNECT_TIMEOUT"),sA=class extends z{constructor(t){super(t),this.name="ConnectTimeoutError",this.message=t||"Connect Timeout Error",this.code="UND_ERR_CONNECT_TIMEOUT"}static[Symbol.hasInstance](t){return t&&t[xp]===!0}[xp]=!0},vp=Symbol.for("undici.error.UND_ERR_HEADERS_TIMEOUT"),rA=class extends z{constructor(t){super(t),this.name="HeadersTimeoutError",this.message=t||"Headers Timeout Error",this.code="UND_ERR_HEADERS_TIMEOUT"}static[Symbol.hasInstance](t){return t&&t[vp]===!0}[vp]=!0},kp=Symbol.for("undici.error.UND_ERR_HEADERS_OVERFLOW"),iA=class extends z{constructor(t){super(t),this.name="HeadersOverflowError",this.message=t||"Headers Overflow Error",this.code="UND_ERR_HEADERS_OVERFLOW"}static[Symbol.hasInstance](t){return t&&t[kp]===!0}[kp]=!0},Dp=Symbol.for("undici.error.UND_ERR_BODY_TIMEOUT"),oA=class extends z{constructor(t){super(t),this.name="BodyTimeoutError",this.message=t||"Body Timeout Error",this.code="UND_ERR_BODY_TIMEOUT"}static[Symbol.hasInstance](t){return t&&t[Dp]===!0}[Dp]=!0},Rp=Symbol.for("undici.error.UND_ERR_RESPONSE_STATUS_CODE"),nA=class extends z{constructor(t,s,r,i){super(t),this.name="ResponseStatusCodeError",this.message=t||"Response Status Code Error",this.code="UND_ERR_RESPONSE_STATUS_CODE",this.body=i,this.status=s,this. Line truncated
`)||e.includes("\r")||e.includes("\0"))===!1}function ax(e,t){let{headersList:s}=t,r=(s.get("referrer-policy",!0)??"").split(","),i="";if(r.length>0)for(let o=r.length;o!==0;o--){let n=r[o-1].trim();if(Vy.has(n)){i=n;break}}i!==""&&(e.referrerPolicy=i)}function Ax(){return"allowed"}function cx(){return"success"}function lx(){return"success"}function ux(e){let t=null;t=e.mode,e.headersList.set("sec-fetch-mode",t,!0)}function px(e){let t=e.origin;if(!(t==="client"||t===void 0)){if(e.responseTainting==="cors"||e.mode==="websocket")e.headersList.append("origin",t,!0);else if(e.method!=="GET"&&e.method!=="HEAD"){switch(e.referrerPolicy){case"no-referrer":t=null;break;case"no-referrer-when-downgrade":case"strict-origin":case"strict-origin-when-cross-origin":e.origin&&KA(e.origin)&&!KA(ai(e))&&(t=null);break;case"same-origin":Yo(e,ai(e))||(t=null);break;default:}e.headersList.append("origin",t,!0)}}}function nr(e,t){return e}function gx(e,t,s){return!e?.startTime||e.startTime<t?{domainLookupStartTime:t,domainLookupEndTime:t,connectionStartTime:t,connectionEndTime:t,secureConnectionStartTime:t,ALPNNegotiatedProtocol:e?.ALPNNegotiatedProtocol}:{domainLookupStartTime:nr(e.domainLookupStartTime,s),domainLookupEndTime:nr(e.domainLookupEndTime,s),connectionStartTime:nr(e.connectionStartTime,s),connectionEndTime:nr(e.connectionEndTime,s),secureConnectionStartTime:nr(e.secureConnectionStartTime,s),ALPNNegotiatedProtocol:e.ALPNNegotiatedProtocol}}function hx(e){return nr(Zy.now(),e)}function dx(e){return{startTime:e.startTime??0,redirectStartTime:0,redirectEndTime:0,postRedirectStartTime:e.startTime??0,finalServiceWorkerStartTime:0,finalNetworkResponseStartTime:0,finalNetworkRequestStartTime:0,endTime:0,encodedBodySize:0,decodedBodySize:0,finalConnectionTimingInfo:null}}function ch(){return{referrerPolicy:"strict-origin-when-cross-origin"}}function Ex(e){return{referrerPolicy:e.referrerPolicy}}function mx(e){let t=e.referrerPolicy;ws(t);let s=null;if(e.referrer==="client"){let a=rh();if(!a||a.origin==="null")return"no-referrer";s=new URL(a)}else e.referrer instanceof URL&&(s=e.referrer);let r=ZA(s),i=ZA(s,!0);r.toString().length>4096&&(r=i);let o=Yo(e,r),n=ni(r)&&!ni(e.url);switch(t){case"origin":return i??ZA(s,!0);case"unsafe-url":return r;case"same-origin":return o?i:"no-referrer";case"origin-when-cross-origin":return o?r:i;case"strict-origin-when-cross-origin":{let a=ai(e);return Yo(r,a)?r:ni(r)&&!ni(a)?"no-referrer":i}default:return n?"no-referrer":i}}function ZA(e,t){return ws(e instanceof URL),e=new URL(e),e.protocol==="file:"||e.protocol==="about:"||e.protocol==="blank:"?"no-referrer":(e.username="",e.password="",e.hash="",t&&(e.pathname="",e.search=""),e)}function ni(e){if(!(e instanceof URL))return!1;if(e.href==="about:blank"||e.href==="about:srcdoc"||e.protocol==="data:"||e.protocol==="file:")return!0;return t(e.origin);function t(s){if(s==null||s==="null")return!1;let r=new URL(s);return!!(r.protocol==="https:"||r.protocol==="wss:"||/^127(?:\.[0-9]+){0,2}\.[0-9]+$|^\[(?:0*:)*?:?0*1\]$/.test(r.hostname)||r.hostname==="localhost"||r.hostname.includes("localhost.")||r.hostname.endsWith(".localhost"))}}function fx(e,t){if(_o===void 0)return!0;let s=lh(t);if(s==="no metadata"||s.length===0)return!0;let r=Bx(s),i=Cx(s,r);for(let o of i){let n=o.algo,a=o.hash,A=_o.createHash(n).update(e).digest("base64");if(A[A.length-1]==="="&&(A[A.length-2]==="="?A=A.slice(0,-2):A=A.slice(0,-1)),Ix(A,a))return!0}return!1}var Qx=/(?<algo>sha256|sha384|sha512)-((?<hash>[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i;function lh(e){let t=[],s=!0;for(let r of e.split(" ")){s=!1;let i=Qx.exec(r);if(i===null||i.groups===void 0||i.groups.algo===void 0)continue;let o=i.groups.algo.toLowerCase();oh.includes(o)&&t.push(i.groups)}return s===!0?"no metadata":t}function Bx(e){let t=e[0].algo;if(t[3]==="5")return t;for(let s=1;s<e.length;++s){let r=e[s];if(r.algo[3]==="5"){t="sha512";break}else{if(t[3]==="3")continue;r.algo[3]==="3"&&(t="sha384")}}return t}function Cx(e,t){if(e.length===1)return e;let s=0;for(let r=0;r<e.length;++r)e[r].algo===t&&(e[s++]=e[r]);return e.length=s,e}function Ix(e,t){if(e.length!==t.length)return!1;for(let s=0;s<e.length;++s)if(e[s]!==t[s]){if(e[s]==="+"&&t[s]==="-"||e[s]==="/"&&t[s]==="_")continue;return!1}return!0}function wx(e){}function Yo(e,t){return e.origin===t.origin&&e.origin==="null"||e.protocol===t.protocol&&e.hostname===t.hostname&&e.port===t.port}function bx(){let e,t;return{promise:new Promise((r,i)=>{e=r,t=i}),resolve:e,reject:t}}function yx(e){return e.controller.state==="aborted"}function xx(e){return e.controller.state==="aborted"||e.controller.state==="terminated"}function vx(e){return $y[e.toLowerCase()]??e}function kx(e){let t=JSON.stringify(e);if(t===void 0)throw new TypeError("Value is not JSON serializable");return ws(typeof t=="string"),t}var Dx=Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()));function uh(e,t,s=0,r=1){class i{#e;#t;#i;constructor(n,a){this.#e=n, Line truncated
`);function ov(e){for(let t=0;t<e.length;++t)if((e.charCodeAt(t)&-128)!==0)return!1;return!0}function nv(e){let t=e.length;if(t<27||t>70)return!1;for(let s=0;s<t;++s){let r=e.charCodeAt(s);if(!(r>=48&&r<=57||r>=65&&r<=90||r>=97&&r<=122||r===39||r===45||r===95))return!1}return!0}function av(e,t){Jo(t!=="failure"&&t.essence==="multipart/form-data");let s=t.parameters.get("boundary");if(s===void 0)return"failure";let r=Buffer.from(`--${s}`,"utf8"),i=[],o={position:0};for(;e[o.position]===13&&e[o.position+1]===10;)o.position+=2;let n=e.length;for(;e[n-1]===10&&e[n-2]===13;)n-=2;for(n!==e.length&&(e=e.subarray(0,n));;){if(e.subarray(o.position,o.position+r.length).equals(r))o.position+=r.length;else return"failure";if(o.position===e.length-2&&Po(e,rv,o)||o.position===e.length-4&&Po(e,iv,o))return i;if(e[o.position]!==13||e[o.position+1]!==10)return"failure";o.position+=2;let a=Av(e,o);if(a==="failure")return"failure";let{name:A,filename:c,contentType:u,encoding:l}=a;o.position+=2;let p;{let h=e.indexOf(r.subarray(2),o.position);if(h===-1)return"failure";p=e.subarray(o.position,h-4),o.position+=p.length,l==="base64"&&(p=Buffer.from(p.toString(),"base64"))}if(e[o.position]!==13||e[o.position+1]!==10)return"failure";o.position+=2;let g;c!==null?(u??="text/plain",ov(u)||(u=""),g=new tv([p],c,{type:u})):g=Zx(Buffer.from(p)),Jo(wh(A)),Jo(typeof g=="string"&&wh(g)||Xx(g)),i.push($x(A,g,c))}}function Av(e,t){let s=null,r=null,i=null,o=null;for(;;){if(e[t.position]===13&&e[t.position+1]===10)return s===null?"failure":{name:s,filename:r,contentType:i,encoding:o};let n=Ar(a=>a!==10&&a!==13&&a!==58,e,t);if(n=ic(n,!0,!0,a=>a===9||a===32),!Kx.test(n.toString())||e[t.position]!==58)return"failure";switch(t.position++,Ar(a=>a===32||a===9,e,t),zx(n)){case"content-disposition":{if(s=r=null,!Po(e,sv,t)||(t.position+=17,s=xh(e,t),s===null))return"failure";if(Po(e,yh,t)){let a=t.position+yh.length;if(e[a]===42&&(t.position+=1,a+=1),e[a]!==61||e[a+1]!==34||(t.position+=12,r=xh(e,t),r===null))return"failure"}break}case"content-type":{let a=Ar(A=>A!==10&&A!==13,e,t);a=ic(a,!1,!0,A=>A===9||A===32),i=bh(a);break}case"content-transfer-encoding":{let a=Ar(A=>A!==10&&A!==13,e,t);a=ic(a,!1,!0,A=>A===9||A===32),o=bh(a);break}default:Ar(a=>a!==10&&a!==13,e,t)}if(e[t.position]!==13&&e[t.position+1]!==10)return"failure";t.position+=2}}function xh(e,t){Jo(e[t.position-1]===34);let s=Ar(r=>r!==10&&r!==13&&r!==34,e,t);return e[t.position]!==34?null:(t.position++,s=new TextDecoder().decode(s).replace(/%0A/ig,`
`).replace(/%0D/ig,"\r").replace(/%22/g,'"'),s)}function Ar(e,t,s){let r=s.position;for(;r<t.length&&e(t[r]);)++r;return t.subarray(s.position,s.position=r)}function ic(e,t,s,r){let i=0,o=e.length-1;if(t)for(;i<e.length&&r(e[i]);)i++;if(s)for(;o>0&&r(e[o]);)o--;return i===0&&o===e.length-1?e:e.subarray(i,o+1)}function Po(e,t,s){if(e.length<t.length)return!1;for(let r=0;r<t.length;r++)if(t[r]!==e[s.position+r])return!1;return!0}vh.exports={multipartFormDataParser:av,validateBoundary:nv}});var ur=Q((C2,Lh)=>{"use strict";var li=U(),{ReadableStreamFrom:cv,isBlobLike:Dh,isReadableStreamLike:lv,readableStreamClose:uv,createDeferredPromise:pv,fullyReadBody:gv,extractMimeType:hv,utf8DecodeBytes:Fh}=Ue(),{FormData:Rh}=ci(),{kState:lr}=Kt(),{webidl:dv}=ge(),{Blob:Ev}=require("node:buffer"),oc=require("node:assert"),{isErrored:Sh,isDisturbed:mv}=require("node:stream"),{isArrayBuffer:fv}=require("node:util/types"),{serializeAMimeType:Qv}=ve(),{multipartFormDataParser:Bv}=kh(),nc;try{let e=require("node:crypto");nc=t=>e.randomInt(0,t)}catch{nc=e=>Math.floor(Math.random(e))}var Ho=new TextEncoder;function Cv(){}var Uh=globalThis.FinalizationRegistry&&process.version.indexOf("v18")!==0,Nh;Uh&&(Nh=new FinalizationRegistry(e=>{let t=e.deref();t&&!t.locked&&!mv(t)&&!Sh(t)&&t.cancel("Response object has been garbage collected").catch(Cv)}));function Gh(e,t=!1){let s=null;e instanceof ReadableStream?s=e:Dh(e)?s=e.stream():s=new ReadableStream({async pull(A){let c=typeof i=="string"?Ho.encode(i):i;c.byteLength&&A.enqueue(c),queueMicrotask(()=>uv(A))},start(){},type:"bytes"}),oc(lv(s));let r=null,i=null,o=null,n=null;if(typeof e=="string")i=e,n="text/plain;charset=UTF-8";else if(e instanceof URLSearchParams)i=e.toString(),n="application/x-www-form-urlencoded;charset=UTF-8";else if(fv(e))i=new Uint8Array(e.slice());else if(ArrayBuffer.isView(e))i=new Uint8Array(e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength));else if(li.isFormDataLike(e)){let A=`----formdata-undici-0${`${nc(1e11)}`.padStart(11,"0")}`,c=`--${A}\r
Content-Disposition: form-data`;let u=m=>m.replace(/\n/g,"%0A").replace(/\r/g,"%0D").replace(/"/g,"%22"),l=m=>m.replace(/\r?\n|\r/g,`\r
`),p=[],g=new Uint8Array([13,10]);o=0;let h=!1;for(let[m,d]of e)if(typeof d=="string"){let f=Ho.encode(c+`; name="${u(l(m))}"\r
\r
${l(d)}\r
`);p.push(f),o+=f.byteLength}else{let f=Ho.encode(`${c}; name="${u(l(m))}"`+(d.name?`; filename="${u(d.name)}"`:"")+`\r
Content-Type: ${d.type||"application/octet-stream"}\r
\r
`);p.push(f,d,g),typeof d.size=="number"?o+=f.byteLength+d.size+g.byteLength:h=!0}let E=Ho.encode(`--${A}--\r
`);p.push(E),o+=E.byteLength,h&&(o=null),i=e,r=async function*(){for(let m of p)m.stream?yield*m.stream():yield m},n=`multipart/form-data; boundary=${A}`}else if(Dh(e))i=e,o=e.size,e.type&&(n=e.type);else if(typeof e[Symbol.asyncIterator]=="function"){if(t)throw new TypeError("keepalive");if(li.isDisturbed(e)||e.locked)throw new TypeError("Response body object should not be disturbed or locked");s=e instanceof ReadableStream?e:cv(e)}if((typeof i=="string"||li.isBuffer(i))&&(o=Buffer.byteLength(i)),r!=null){let A;s=new ReadableStream({async start(){A=r(e)[Symbol.asyncIterator]()},async pull(c){let{value:u,done:l}=await A.next();if(l)queueMicrotask(()=>{c.close(),c.byobRequest?.respond(0)});else if(!Sh(s)){let p=new Uint8Array(u);p.byteLength&&c.enqueue(p)}return c.desiredSize>0},async cancel(c){await A.return()},type:"bytes"})}return[{stream:s,source:i,length:o},n]}function Iv(e,t=!1){return e instanceof ReadableStream&&(oc(!li.isDisturbed(e),"The body has already been consumed."),oc(!e.locked,"The stream is locked.")),Gh(e,t)}function wv(e,t){let[s,r]=t.stream.tee();return t.stream=s,{stream:r,length:t.length,source:t.source}}function bv(e){if(e.aborted)throw new DOMException("The operation was aborted.","AbortError")}function yv(e){return{blob(){return cr(this,s=>{let r=Th(this);return r===null?r="":r&&(r=Qv(r)),new Ev([s],{type:r})},e)},arrayBuffer(){return cr(this,s=>new Uint8Array(s).buffer,e)},text(){return cr(this,Fh,e)},json(){return cr(this,vv,e)},formData(){return cr(this,s=>{let r=Th(this);if(r!==null)switch(r.essence){case"multipart/form-data":{let i=Bv(s,r);if(i==="failure")throw new TypeError("Failed to parse body as FormData.");let o=new Rh;return o[lr]=i,o}case"application/x-www-form-urlencoded":{let i=new URLSearchParams(s.toString()),o=new Rh;for(let[n,a]of i)o.append(n,a);return o}}throw new TypeError('Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".')},e)},bytes(){return cr(this,s=>new Uint8Array(s),e)}}}function xv(e){Object.assign(e.prototype,yv(e))}async function cr(e,t,s){if(dv.brandCheck(e,s),Mh(e))throw new TypeError("Body is unusable: Body has already been read");bv(e[lr]);let r=pv(),i=n=>r.reject(n),o=n=>{try{r.resolve(t(n))}catch(a){i(a)}};return e[lr].body==null?(o(Buffer.allocUnsafe(0)),r.promise):(await gv(e[lr].body,o,i),r.promise)}function Mh(e){let t=e[lr].body;return t!=null&&(t.stream.locked||li.isDisturbed(t.stream))}function vv(e){return JSON.parse(Fh(e))}function Th(e){let t=e[lr].headersList,s=hv(t);return s==="failure"?null:s}Lh.exports={extractBody:Gh,safelyExtractBody:Iv,cloneBody:wv,mixinBody:xv,streamRegistry:Nh,hasFinalizationRegistry:Uh,bodyUnusable:Mh}});var zh=Q((I2,jh)=>{"use strict";var k=require("node:assert"),F=U(),{channels:_h}=Xs(),ac=JA(),{RequestContentLengthMismatchError:bs,ResponseContentLengthMismatchError:kv,RequestAbortedError:Vh,HeadersTimeoutError:Dv,HeadersOverflowError:Rv,SocketError:Zo,InformationalError:pr,BodyTimeoutError:Tv,HTTPParserError:Fv,ResponseExceededMaxSizeError:Sv}=L(),{kUrl:qh,kReset:ke,kClient:uc,kParser:X,kBlocking:gi,kRunning:Ee,kPending:Uv,kSize:Yh,kWriting:$t,kQueue:st,kNoRef:ui,kKeepAliveDefaultTimeout:Nv,kHostHeader:Gv,kPendingIdx:Mv,kRunningIdx:qe,kError:We,kPipelining:jo,kSocket:gr,kKeepAliveTimeoutValue:Ko,kMaxHeadersSize:Ac,kKeepAliveMaxTimeout:Lv,kKeepAliveTimeoutThreshold:_v,kHeadersTimeout:Yv,kBodyTimeout:Ov,kStrictContentLength:pc,kMaxRequests:Oh,kCounter:Jv,kMaxResponseSize:Pv,kOnError:Hv,kResume:Xt,kHTTPContext:Wh}=j(),ft=Ng(),Vv=Buffer.alloc(0),Vo=Buffer[Symbol.species],qo=F.addListener,qv=F.removeAllListeners,cc;async function Wv(){let e=process.env.JEST_WORKER_ID?VA():void 0,t;try{t=await WebAssembly.compile(Lg())}catch{t=await WebAssembly.compile(e||VA())}return await WebAssembly.instantiate(t,{env:{wasm_on_url:(s,r,i)=>0,wasm_on_status:(s,r,i)=>{k(oe.ptr===s);let o=r-Bt+Qt.byteOffset;return oe.onStatus(new Vo(Qt.buffer,o,i))||0},wasm_on_message_begin:s=>(k(oe.ptr===s),oe.onMessageBegin()||0),wasm_on_header_field:(s,r,i)=>{k(oe.ptr===s);let o=r-Bt+Qt.byteOffset;return oe.onHeaderField(new Vo(Qt.buffer,o,i))||0},wasm_on_header_value:(s,r,i)=>{k(oe.ptr===s);let o=r-Bt+Qt.byteOffset;return oe.onHeaderValue(new Vo(Qt.buffer,o,i))||0},wasm_on_headers_complete:(s,r,i,o)=>(k(oe.ptr===s),oe.onHeadersComplete(r,!!i,!!o)||0),wasm_on_body:(s,r,i)=>{k(oe.ptr===s);let o=r-Bt+Qt.byteOffset;return oe.onBody(new Vo(Qt.buffer,o,i))||0},wasm_on_message_complete:s=>(k(oe.ptr===s),oe.onMessageComplete()||0)}})}var lc=null,gc=Wv();gc.catch();var oe=null,Qt=null,Wo=0,Bt=null,jv=0,pi=1,hr=2|pi,zo=4|pi,hc=8|jv,dc=class{constructor(t,s,{exports:r}){k(Number.isFinite(t[Ac])&&t[Ac]>0),this.llhttp=r,this.ptr=this.llhttp.llhttp_alloc(ft.TYPE.RESPONSE),this.client=t,this.socket=s,this.timeout=null,this.timeoutValue=null,this.timeoutType=null,this.statusCode=null,this.statusText="",this.upgrade=!1,this.headers=[],this.headersSize=0,this.headersMaxSize=t[Ac],this.shouldKeepAlive=!1,this.paused=!1, Line truncated
`;if(typeof i=="string"?E+=`host: ${i}\r
`:E+=e[Gv],o?E+=`connection: upgrade\r
upgrade: ${o}\r
`:e[jo]&&!g[ke]?E+=`connection: keep-alive\r
`:E+=`connection: close\r
`,Array.isArray(c))for(let m=0;m<c.length;m+=2){let d=c[m+0],f=c[m+1];if(Array.isArray(f))for(let C=0;C<f.length;C++)E+=`${d}: ${f[C]}\r
`;else E+=`${d}: ${f}\r
`}return _h.sendHeaders.hasSubscribers&&_h.sendHeaders.publish({request:t,headers:E,socket:g}),!A||p===0?Ph(h,null,e,t,g,u,E,l):F.isBuffer(A)?Ph(h,A,e,t,g,u,E,l):F.isBlobLike(A)?typeof A.stream=="function"?Hh(h,A.stream(),e,t,g,u,E,l):ek(h,A,e,t,g,u,E,l):F.isStream(A)?$v(h,A,e,t,g,u,E,l):F.isIterable(A)?Hh(h,A,e,t,g,u,E,l):k(!1),!0}function $v(e,t,s,r,i,o,n,a){k(o!==0||s[Ee]===0,"stream body cannot be pipelined");let A=!1,c=new Xo({abort:e,socket:i,request:r,contentLength:o,client:s,expectsPayload:a,header:n}),u=function(h){if(!A)try{!c.write(h)&&this.pause&&this.pause()}catch(E){F.destroy(this,E)}},l=function(){A||t.resume&&t.resume()},p=function(){if(queueMicrotask(()=>{t.removeListener("error",g)}),!A){let h=new Vh;queueMicrotask(()=>g(h))}},g=function(h){if(!A){if(A=!0,k(i.destroyed||i[$t]&&s[Ee]<=1),i.off("drain",l).off("error",g),t.removeListener("data",u).removeListener("end",g).removeListener("close",p),!h)try{c.end()}catch(E){h=E}c.destroy(h),h&&(h.code!=="UND_ERR_INFO"||h.message!=="reset")?F.destroy(t,h):F.destroy(t)}};t.on("data",u).on("end",g).on("error",g).on("close",p),t.resume&&t.resume(),i.on("drain",l).on("error",g),t.errorEmitted??t.errored?setImmediate(()=>g(t.errored)):(t.endEmitted??t.readableEnded)&&setImmediate(()=>g(null)),(t.closeEmitted??t.closed)&&setImmediate(p)}function Ph(e,t,s,r,i,o,n,a){try{t?F.isBuffer(t)&&(k(o===t.byteLength,"buffer body must have content length"),i.cork(),i.write(`${n}content-length: ${o}\r
\r
`,"latin1"),i.write(t),i.uncork(),r.onBodySent(t),!a&&r.reset!==!1&&(i[ke]=!0)):o===0?i.write(`${n}content-length: 0\r
\r
`,"latin1"):(k(o===null,"no body must not have content length"),i.write(`${n}\r
`,"latin1")),r.onRequestSent(),s[Xt]()}catch(A){e(A)}}async function ek(e,t,s,r,i,o,n,a){k(o===t.size,"blob body must have content length");try{if(o!=null&&o!==t.size)throw new bs;let A=Buffer.from(await t.arrayBuffer());i.cork(),i.write(`${n}content-length: ${o}\r
\r
`,"latin1"),i.write(A),i.uncork(),r.onBodySent(A),r.onRequestSent(),!a&&r.reset!==!1&&(i[ke]=!0),s[Xt]()}catch(A){e(A)}}async function Hh(e,t,s,r,i,o,n,a){k(o!==0||s[Ee]===0,"iterator body cannot be pipelined");let A=null;function c(){if(A){let p=A;A=null,p()}}let u=()=>new Promise((p,g)=>{k(A===null),i[We]?g(i[We]):A=p});i.on("close",c).on("drain",c);let l=new Xo({abort:e,socket:i,request:r,contentLength:o,client:s,expectsPayload:a,header:n});try{for await(let p of t){if(i[We])throw i[We];l.write(p)||await u()}l.end()}catch(p){l.destroy(p)}finally{i.off("close",c).off("drain",c)}}var Xo=class{constructor({abort:t,socket:s,request:r,contentLength:i,client:o,expectsPayload:n,header:a}){this.socket=s,this.request=r,this.contentLength=i,this.client=o,this.bytesWritten=0,this.expectsPayload=n,this.header=a,this.abort=t,s[$t]=!0}write(t){let{socket:s,request:r,contentLength:i,client:o,bytesWritten:n,expectsPayload:a,header:A}=this;if(s[We])throw s[We];if(s.destroyed)return!1;let c=Buffer.byteLength(t);if(!c)return!0;if(i!==null&&n+c>i){if(o[pc])throw new bs;process.emitWarning(new bs)}s.cork(),n===0&&(!a&&r.reset!==!1&&(s[ke]=!0),i===null?s.write(`${A}transfer-encoding: chunked\r
`,"latin1"):s.write(`${A}content-length: ${i}\r
\r
`,"latin1")),i===null&&s.write(`\r
${c.toString(16)}\r
`,"latin1"),this.bytesWritten+=c;let u=s.write(t);return s.uncork(),r.onBodySent(t),u||s[X].timeout&&s[X].timeoutType===hr&&s[X].timeout.refresh&&s[X].timeout.refresh(),u}end(){let{socket:t,contentLength:s,client:r,bytesWritten:i,expectsPayload:o,header:n,request:a}=this;if(a.onRequestSent(),t[$t]=!1,t[We])throw t[We];if(!t.destroyed){if(i===0?o?t.write(`${n}content-length: 0\r
\r
`,"latin1"):t.write(`${n}\r
`,"latin1"):s===null&&t.write(`\r
0\r
\r
`,"latin1"),s!==null&&i!==s){if(r[pc])throw new bs;process.emitWarning(new bs)}t[X].timeout&&t[X].timeoutType===hr&&t[X].timeout.refresh&&t[X].timeout.refresh(),r[Xt]()}}destroy(t){let{socket:s,client:r,abort:i}=this;s[$t]=!1,t&&(k(r[Ee]<=1,"pipeline should only contain this request"),i(t))}};jh.exports=zv});var rd=Q((w2,sd)=>{"use strict";var je=require("node:assert"),{pipeline:tk}=require("node:stream"),N=U(),{RequestContentLengthMismatchError:Ec,RequestAbortedError:Zh,SocketError:hi,InformationalError:mc}=L(),{kUrl:$o,kReset:tn,kClient:dr,kRunning:sn,kPending:sk,kQueue:es,kPendingIdx:fc,kRunningIdx:rt,kError:ot,kSocket:ae,kStrictContentLength:rk,kOnError:Qc,kMaxConcurrentStreams:td,kHTTP2Session:it,kResume:ts,kSize:ik,kHTTPContext:ok}=j(),Gt=Symbol("open streams"),Kh,Xh=!1,en;try{en=require("node:http2")}catch{en={constants:{}}}var{constants:{HTTP2_HEADER_AUTHORITY:nk,HTTP2_HEADER_METHOD:ak,HTTP2_HEADER_PATH:Ak,HTTP2_HEADER_SCHEME:ck,HTTP2_HEADER_CONTENT_LENGTH:lk,HTTP2_HEADER_EXPECT:uk,HTTP2_HEADER_STATUS:pk}}=en;function gk(e){let t=[];for(let[s,r]of Object.entries(e))if(Array.isArray(r))for(let i of r)t.push(Buffer.from(s),Buffer.from(i));else t.push(Buffer.from(s),Buffer.from(r));return t}async function hk(e,t){e[ae]=t,Xh||(Xh=!0,process.emitWarning("H2 support is experimental, expect them to change at any time.",{code:"UNDICI-H2"}));let s=en.connect(e[$o],{createConnection:()=>t,peerMaxConcurrentStreams:e[td]});s[Gt]=0,s[dr]=e,s[ae]=t,N.addListener(s,"error",Ek),N.addListener(s,"frameError",mk),N.addListener(s,"end",fk),N.addListener(s,"goaway",Qk),N.addListener(s,"close",function(){let{[dr]:i}=this,{[ae]:o}=i,n=this[ae][ot]||this[ot]||new hi("closed",N.getSocketInfo(o));if(i[it]=null,i.destroyed){je(i[sk]===0);let a=i[es].splice(i[rt]);for(let A=0;A<a.length;A++){let c=a[A];N.errorRequest(i,c,n)}}}),s.unref(),e[it]=s,t[it]=s,N.addListener(t,"error",function(i){je(i.code!=="ERR_TLS_CERT_ALTNAME_INVALID"),this[ot]=i,this[dr][Qc](i)}),N.addListener(t,"end",function(){N.destroy(this,new hi("other side closed",N.getSocketInfo(this)))}),N.addListener(t,"close",function(){let i=this[ot]||new hi("closed",N.getSocketInfo(this));e[ae]=null,this[it]!=null&&this[it].destroy(i),e[fc]=e[rt],je(e[sn]===0),e.emit("disconnect",e[$o],[e],i),e[ts]()});let r=!1;return t.on("close",()=>{r=!0}),{version:"h2",defaultPipelining:1/0,write(...i){return Ck(e,...i)},resume(){dk(e)},destroy(i,o){r?queueMicrotask(o):t.destroy(i).on("close",o)},get destroyed(){return t.destroyed},busy(){return!1}}}function dk(e){let t=e[ae];t?.destroyed===!1&&(e[ik]===0&&e[td]===0?(t.unref(),e[it].unref()):(t.ref(),e[it].ref()))}function Ek(e){je(e.code!=="ERR_TLS_CERT_ALTNAME_INVALID"),this[ae][ot]=e,this[dr][Qc](e)}function mk(e,t,s){if(s===0){let r=new mc(`HTTP/2: "frameError" received - type ${e}, code ${t}`);this[ae][ot]=r,this[dr][Qc](r)}}function fk(){let e=new hi("other side closed",N.getSocketInfo(this[ae]));this.destroy(e),N.destroy(this[ae],e)}function Qk(e){let t=this[ot]||new hi(`HTTP/2: "GOAWAY" frame received with code ${e}`,N.getSocketInfo(this)),s=this[dr];if(s[ae]=null,s[ok]=null,this[it]!=null&&(this[it].destroy(t),this[it]=null),N.destroy(this[ae],t),s[rt]<s[es].length){let r=s[es][s[rt]];s[es][s[rt]++]=null,N.errorRequest(s,r,t),s[fc]=s[rt]}je(s[sn]===0),s.emit("disconnect",s[$o],[s],t),s[ts]()}function Bk(e){return e!=="GET"&&e!=="HEAD"&&e!=="OPTIONS"&&e!=="TRACE"&&e!=="CONNECT"}function Ck(e,t){let s=e[it],{method:r,path:i,host:o,upgrade:n,expectContinue:a,signal:A,headers:c}=t,{body:u}=t;if(n)return N.errorRequest(e,t,new Error("Upgrade not supported for H2")),!1;let l={};for(let B=0;B<c.length;B+=2){let b=c[B+0],Y=c[B+1];if(Array.isArray(Y))for(let O=0;O<Y.length;O++)l[b]?l[b]+=`,${Y[O]}`:l[b]=Y[O];else l[b]=Y}let p,{hostname:g,port:h}=e[$o];l[nk]=o||`${g}${h?`:${h}`:""}`,l[ak]=r;let E=B=>{t.aborted||t.completed||(B=B||new Zh,N.errorRequest(e,t,B),p!=null&&N.destroy(p,B),N.destroy(u,B),e[es][e[rt]++]=null,e[ts]())};try{t.onConnect(E)}catch(B){N.errorRequest(e,t,B)}if(t.aborted)return!1;if(r==="CONNECT")return s.ref(),p=s.request(l,{endStream:!1,signal:A}),p.id&&!p.pending?(t.onUpgrade(null,null,p),++s[Gt],e[es][e[rt]++]=null):p.once("ready",()=>{t.onUpgrade(null,null,p),++s[Gt],e[es][e[rt]++]=null}),p.once("close",()=>{s[Gt]-=1,s[Gt]===0&&s.unref()}),!0;l[Ak]=i,l[ck]="https";let m=r==="PUT"||r==="POST"||r==="PATCH";u&&typeof u.read=="function"&&u.read(0);let d=N.bodyLength(u);if(N.isFormDataLike(u)){Kh??=ur().extractBody;let[B,b]=Kh(u);l["content-type"]=b,u=B.stream,d=B.length}if(d==null&&(d=t.contentLength),(d===0||!m)&&(d=null),Bk(r)&&d>0&&t.contentLength!=null&&t.contentLength!==d){if(e[rk])return N.errorRequest(e,t,new Ec),!1;process.emitWarning(new Ec)}d!=null&&(je(u,"no body must not have content length"),l[lk]=`${d}`),s.ref();let f=r==="GET"||r==="HEAD"||u===null;return a?(l[uk]="100-continue",p=s.request(l,{endStream:f,signal:A}),p.once("continue",C)):(p=s.request(l,{endStream:f,signal:A}),C()),++s[Gt],p.once("r Line truncated
`,this[Wk]=A??3e5,this[qk]=i??3e5,this[jk]=f??!0,this[zk]=B,this[wc]=Y,this[rs]=null,this[e0]=pe>-1?pe:-1,this[s0]=Wt??100,this[ee]=null,this[nt]=[],this[Lt]=0,this[at]=0,this[fi]=jt=>yc(this,jt),this[t0]=jt=>dd(this,jt)}get pipelining(){return this[an]}set pipelining(t){this[an]=t,this[fi](!0)}get[Ci](){return this[nt].length-this[at]}get[Bi](){return this[at]-this[Lt]}get[Qi](){return this[nt].length-this[Lt]}get[_k](){return!!this[ee]&&!this[mr]&&!this[ee].destroyed}get[Ic](){return!!(this[ee]?.busy(null)||this[Qi]>=(hd(this)||1)||this[Ci]>0)}[Lk](t){Ed(this),this.once("connect",t)}[$k](t,s){let r=t.origin||this[It].origin,i=new Fk(r,t,s);return this[nt].push(i),this[xs]||(ys.bodyLength(i.body)==null&&ys.isIterable(i.body)?(this[xs]=1,queueMicrotask(()=>yc(this))):this[fi](!0)),this[xs]&&this[is]!==2&&this[Ic]&&(this[is]=2),this[is]<2}async[Kk](){return new Promise(t=>{this[Qi]?this[rs]=t:t(null)})}async[Xk](t){return new Promise(s=>{let r=this[nt].splice(this[at]);for(let o=0;o<r.length;o++){let n=r[o];ys.errorRequest(this,n,t)}let i=()=>{this[rs]&&(this[rs](),this[rs]=null),s(null)};this[ee]?(this[ee].destroy(t,i),this[ee]=null):queueMicrotask(i),this[fi]()})}},o0=nn();function dd(e,t){if(e[Bi]===0&&t.code!=="UND_ERR_INFO"&&t.code!=="UND_ERR_SOCKET"){Mt(e[at]===e[Lt]);let s=e[nt].splice(e[Lt]);for(let r=0;r<s.length;r++){let i=s[r];ys.errorRequest(e,i,t)}Mt(e[Qi]===0)}}async function Ed(e){Mt(!e[mr]),Mt(!e[ee]);let{host:t,hostname:s,protocol:r,port:i}=e[It];if(s[0]==="["){let o=s.indexOf("]");Mt(o!==-1);let n=s.substring(1,o);Mt(gd.isIP(n)),s=n}e[mr]=!0,Er.beforeConnect.hasSubscribers&&Er.beforeConnect.publish({connectParams:{host:t,hostname:s,protocol:r,port:i,version:e[ee]?.version,servername:e[ss],localAddress:e[mi]},connector:e[Ei]});try{let o=await new Promise((n,a)=>{e[Ei]({host:t,hostname:s,protocol:r,port:i,servername:e[ss],localAddress:e[mi]},(A,c)=>{A?a(A):n(c)})});if(e.destroyed){ys.destroy(o.on("error",ud),new Nk);return}Mt(o);try{e[ee]=o.alpnProtocol==="h2"?await i0(e,o):await r0(e,o)}catch(n){throw o.destroy().on("error",ud),n}e[mr]=!1,o[Zk]=0,o[wc]=e[wc],o[Mk]=e,o[Ok]=null,Er.connected.hasSubscribers&&Er.connected.publish({connectParams:{host:t,hostname:s,protocol:r,port:i,version:e[ee]?.version,servername:e[ss],localAddress:e[mi]},connector:e[Ei],socket:o}),e.emit("connect",e[It],[e])}catch(o){if(e.destroyed)return;if(e[mr]=!1,Er.connectError.hasSubscribers&&Er.connectError.publish({connectParams:{host:t,hostname:s,protocol:r,port:i,version:e[ee]?.version,servername:e[ss],localAddress:e[mi]},connector:e[Ei],error:o}),o.code==="ERR_TLS_CERT_ALTNAME_INVALID")for(Mt(e[Bi]===0);e[Ci]>0&&e[nt][e[at]].servername===e[ss];){let n=e[nt][e[at]++];ys.errorRequest(e,n,o)}else dd(e,o);e.emit("connectionError",e[It],[e],o)}e[fi]()}function pd(e){e[is]=0,e.emit("drain",e[It],[e])}function yc(e,t){e[xs]!==2&&(e[xs]=2,n0(e,t),e[xs]=0,e[Lt]>256&&(e[nt].splice(0,e[Lt]),e[at]-=e[Lt],e[Lt]=0))}function n0(e,t){for(;;){if(e.destroyed){Mt(e[Ci]===0);return}if(e[rs]&&!e[Qi]){e[rs](),e[rs]=null;return}if(e[ee]&&e[ee].resume(),e[Ic])e[is]=2;else if(e[is]===2){t?(e[is]=1,queueMicrotask(()=>pd(e))):pd(e);continue}if(e[Ci]===0||e[Bi]>=(hd(e)||1))return;let s=e[nt][e[at]];if(e[It].protocol==="https:"&&e[ss]!==s.servername){if(e[Bi]>0)return;e[ss]=s.servername,e[ee]?.destroy(new Uk("servername changed"),()=>{e[ee]=null,yc(e)})}if(e[mr])return;if(!e[ee]){Ed(e);return}if(e[ee].destroyed||e[ee].busy(s))return;!s.aborted&&e[ee].write(s)?e[at]++:e[nt].splice(e[at],1)}}md.exports=bc});var xc=Q((k2,fd)=>{"use strict";var An=class{constructor(){this.bottom=0,this.top=0,this.list=new Array(2048),this.next=null}isEmpty(){return this.top===this.bottom}isFull(){return(this.top+1&2047)===this.bottom}push(t){this.list[this.top]=t,this.top=this.top+1&2047}shift(){let t=this.list[this.bottom];return t===void 0?null:(this.list[this.bottom]=void 0,this.bottom=this.bottom+1&2047,t)}};fd.exports=class{constructor(){this.head=this.tail=new An}isEmpty(){return this.head.isEmpty()}push(t){this.head.isFull()&&(this.head=this.head.next=new An),this.head.push(t)}shift(){let t=this.tail,s=t.shift();return t.isEmpty()&&t.next!==null&&(this.tail=t.next),s}}});var Bd=Q((D2,Qd)=>{var{kFree:a0,kConnected:A0,kPending:c0,kQueued:l0,kRunning:u0,kSize:p0}=j(),vs=Symbol("pool"),vc=class{constructor(t){this[vs]=t}get connected(){return this[vs][A0]}get free(){return this[vs][a0]}get pending(){return this[vs][c0]}get queued(){return this[vs][l0]}get running(){return this[vs][u0]}get size(){return this[vs][p0]}};Qd.exports=vc});var Sc=Q((R2,Rd)=>{"use strict";var g0=sr(),h0=xc(),{kConnected:kc,kSize:Cd,kRunning:Id,kPending:wd,kQueued:Ii,kBusy:d0,kFree:E0,kUrl:m0,kClose:f0,kDestroy:Q0,kDispatch:B0}=j(),C0=Bd(),De=Symbol("clients"),Ce=Symbol("needDrain"),wi=Symbol("queue"),Dc=Symbol("closed resolve"),Rc=Symbol("onDrain"),bd=Symbol("onConnect"),yd=Symbol("onDisconnect"),xd=Symbol("onConnectionError"),Tc=Symbol("get dispatcher"),kd=Symbol("add client"),Dd=Sym Line truncated
${r.count} ${r.noun} ${r.is} pending:
${t.format(s)}
`.trim())}};Gm.exports=Sl});var xn=Q((iY,Om)=>{"use strict";var Lm=Symbol.for("undici.globalDispatcher.1"),{InvalidArgumentError:WR}=L(),jR=Br();Ym()===void 0&&_m(new jR);function _m(e){if(!e||typeof e.dispatch!="function")throw new WR("Argument agent must implement Agent");Object.defineProperty(globalThis,Lm,{value:e,writable:!0,enumerable:!1,configurable:!1})}function Ym(){return globalThis[Lm]}Om.exports={setGlobalDispatcher:_m,getGlobalDispatcher:Ym}});var vn=Q((nY,Jm)=>{"use strict";Jm.exports=class{#e;constructor(t){if(typeof t!="object"||t===null)throw new TypeError("handler must be an object");this.#e=t}onConnect(...t){return this.#e.onConnect?.(...t)}onError(...t){return this.#e.onError?.(...t)}onUpgrade(...t){return this.#e.onUpgrade?.(...t)}onResponseStarted(...t){return this.#e.onResponseStarted?.(...t)}onHeaders(...t){return this.#e.onHeaders?.(...t)}onData(...t){return this.#e.onData?.(...t)}onComplete(...t){return this.#e.onComplete?.(...t)}onBodySent(...t){return this.#e.onBodySent?.(...t)}}});var Hm=Q((aY,Pm)=>{"use strict";var zR=on();Pm.exports=e=>{let t=e?.maxRedirections;return s=>function(i,o){let{maxRedirections:n=t,...a}=i;if(!n)return s(i,o);let A=new zR(s,n,i,o);return s(a,A)}}});var qm=Q((AY,Vm)=>{"use strict";var ZR=En();Vm.exports=e=>t=>function(r,i){return t(r,new ZR({...r,retryOptions:{...e,...r.retryOptions}},{handler:i,dispatch:t}))}});var jm=Q((cY,Wm)=>{"use strict";var KR=U(),{InvalidArgumentError:XR,RequestAbortedError:$R}=L(),eT=vn(),Ul=class extends eT{#e=1024*1024;#t=null;#i=!1;#s=!1;#r=0;#A=null;#a=null;constructor({maxSize:t},s){if(super(s),t!=null&&(!Number.isFinite(t)||t<1))throw new XR("maxSize must be a number greater than 0");this.#e=t??this.#e,this.#a=s}onConnect(t){this.#t=t,this.#a.onConnect(this.#c.bind(this))}#c(t){this.#s=!0,this.#A=t}onHeaders(t,s,r,i){let n=KR.parseHeaders(s)["content-length"];if(n!=null&&n>this.#e)throw new $R(`Response size (${n}) larger than maxSize (${this.#e})`);return this.#s?!0:this.#a.onHeaders(t,s,r,i)}onError(t){this.#i||(t=this.#A??t,this.#a.onError(t))}onData(t){return this.#r=this.#r+t.length,this.#r>=this.#e&&(this.#i=!0,this.#s?this.#a.onError(this.#A):this.#a.onComplete([])),!0}onComplete(t){if(!this.#i){if(this.#s){this.#a.onError(this.reason);return}this.#a.onComplete(t)}}};function tT({maxSize:e}={maxSize:1024*1024}){return t=>function(r,i){let{dumpMaxSize:o=e}=r,n=new Ul({maxSize:o},i);return t(r,n)}}Wm.exports=tT});var Km=Q((lY,Zm)=>{"use strict";var{isIP:sT}=require("node:net"),{lookup:rT}=require("node:dns"),iT=vn(),{InvalidArgumentError:Rr,InformationalError:oT}=L(),zm=Math.pow(2,31)-1,Nl=class{#e=0;#t=0;#i=new Map;dualStack=!0;affinity=null;lookup=null;pick=null;constructor(t){this.#e=t.maxTTL,this.#t=t.maxItems,this.dualStack=t.dualStack,this.affinity=t.affinity,this.lookup=t.lookup??this.#s,this.pick=t.pick??this.#r}get full(){return this.#i.size===this.#t}runLookup(t,s,r){let i=this.#i.get(t.hostname);if(i==null&&this.full){r(null,t.origin);return}let o={affinity:this.affinity,dualStack:this.dualStack,lookup:this.lookup,pick:this.pick,...s.dns,maxTTL:this.#e,maxItems:this.#t};if(i==null)this.lookup(t,o,(n,a)=>{if(n||a==null||a.length===0){r(n??new oT("No DNS entries found"));return}this.setRecords(t,a);let A=this.#i.get(t.hostname),c=this.pick(t,A,o.affinity),u;typeof c.port=="number"?u=`:${c.port}`:t.port!==""?u=`:${t.port}`:u="",r(null,`${t.protocol}//${c.family===6?`[${c.address}]`:c.address}${u}`)});else{let n=this.pick(t,i,o.affinity);if(n==null){this.#i.delete(t.hostname),this.runLookup(t,s,r);return}let a;typeof n.port=="number"?a=`:${n.port}`:t.port!==""?a=`:${t.port}`:a="",r(null,`${t.protocol}//${n.family===6?`[${n.address}]`:n.address}${a}`)}}#s(t,s,r){rT(t.hostname,{all:!0,family:this.dualStack===!1?this.affinity:0,order:"ipv4first"},(i,o)=>{if(i)return r(i);let n=new Map;for(let a of o)n.set(`${a.address}:${a.family}`,a);r(null,n.values())})}#r(t,s,r){let i=null,{records:o,offset:n}=s,a;if(this.dualStack?(r==null&&(n==null||n===zm?(s.offset=0,r=4):(s.offset++,r=(s.offset&1)===1?6:4)),o[r]!=null&&o[r].ips.length>0?a=o[r]:a=o[r===4?6:4]):a=o[r],a==null||a.ips.length===0)return i;a.offset==null||a.offset===zm?a.offset=0:a.offset++;let A=a.offset%a.ips.length;return i=a.ips[A]??null,i==null?i:Date.now()-i.timestamp>i.ttl?(a.ips.splice(A,1),this.pick(t,s,r)):i}setRecords(t,s){let r=Date.now(),i={records:{4:null,6:null}};for(let o of s){o.timestamp=r,typeof o.ttl=="number"?o.ttl=Math.min(o.ttl,this.#e):o.ttl=this.#e;let n=i.records[o.family]??{ips:[]};n.ips.push(o),i.records[o.family]=n}this.#i.set(t.hostname,i)}getHandler(t,s){return new Gl(this,t,s)}},Gl=class extends iT{#e=null;#t=null;#i=null;#s=null;#r=null;constructor(t,{origin:s,handler:r,dispatch:i},o){super(r),this.#r=s,this.#s=r,this.#t={...o},this.#e=t,this.#i=i}onError(t){switch(t.code){case"ETIMEDOUT":case"ECONNREFUSED":{if(this.#e.dualStack){this.#e.runLookup(this.#r,this.#t,(s,r)=>{if(s)return this.#s.onError(s);let i={...this.#t,origin: Line truncated
${o}`;break;case"retry":TB(o)&&(s[i]=o);break;case"id":FB(o)&&(s[i]=o);break;case"event":o.length>0&&(s[i]=o);break}}processEvent(t){t.retry&&TB(t.retry)&&(this.state.reconnectionTime=parseInt(t.retry,10)),t.id&&FB(t.id)&&(this.state.lastEventId=t.id),t.data!==void 0&&this.push({type:t.event||"message",options:{data:t.data,lastEventId:this.state.lastEventId,origin:this.state.origin}})}clearEvent(){this.event={data:void 0,event:void 0,id:void 0,retry:void 0}}};SB.exports={EventSourceStream:Cu}});var JB=Q((OY,OB)=>{"use strict";var{pipeline:dN}=require("node:stream"),{fetching:EN}=Gi(),{makeRequest:mN}=Sr(),{webidl:Pt}=ge(),{EventSourceStream:fN}=UB(),{parseMIMEType:QN}=ve(),{createFastMessageEvent:BN}=_r(),{isNetworkError:NB}=Ui(),{delay:CN}=Qu(),{kEnumerableProperty:Js}=U(),{environmentSettingsObject:GB}=Ue(),MB=!1,LB=3e3,zi=0,_B=1,Zi=2,IN="anonymous",wN="use-credentials",Hr=class e extends EventTarget{#e={open:null,error:null,message:null};#t=null;#i=!1;#s=zi;#r=null;#A=null;#a;#c;constructor(t,s={}){super(),Pt.util.markAsUncloneable(this);let r="EventSource constructor";Pt.argumentLengthCheck(arguments,1,r),MB||(MB=!0,process.emitWarning("EventSource is experimental, expect them to change at any time.",{code:"UNDICI-ES"})),t=Pt.converters.USVString(t,r,"url"),s=Pt.converters.EventSourceInitDict(s,r,"eventSourceInitDict"),this.#a=s.dispatcher,this.#c={lastEventId:"",reconnectionTime:LB};let i=GB,o;try{o=new URL(t,i.settingsObject.baseUrl),this.#c.origin=o.origin}catch(A){throw new DOMException(A,"SyntaxError")}this.#t=o.href;let n=IN;s.withCredentials&&(n=wN,this.#i=!0);let a={redirect:"follow",keepalive:!0,mode:"cors",credentials:n==="anonymous"?"same-origin":"omit",referrer:"no-referrer"};a.client=GB.settingsObject,a.headersList=[["accept",{name:"accept",value:"text/event-stream"}]],a.cache="no-store",a.initiator="other",a.urlList=[new URL(this.#t)],this.#r=mN(a),this.#l()}get readyState(){return this.#s}get url(){return this.#t}get withCredentials(){return this.#i}#l(){if(this.#s===Zi)return;this.#s=zi;let t={request:this.#r,dispatcher:this.#a},s=r=>{NB(r)&&(this.dispatchEvent(new Event("error")),this.close()),this.#g()};t.processResponseEndOfBody=s,t.processResponse=r=>{if(NB(r))if(r.aborted){this.close(),this.dispatchEvent(new Event("error"));return}else{this.#g();return}let i=r.headersList.get("content-type",!0),o=i!==null?QN(i):"failure",n=o!=="failure"&&o.essence==="text/event-stream";if(r.status!==200||n===!1){this.close(),this.dispatchEvent(new Event("error"));return}this.#s=_B,this.dispatchEvent(new Event("open")),this.#c.origin=r.urlList[r.urlList.length-1].origin;let a=new fN({eventSourceSettings:this.#c,push:A=>{this.dispatchEvent(BN(A.type,A.options))}});dN(r.body.stream,a,A=>{A?.aborted===!1&&(this.close(),this.dispatchEvent(new Event("error")))})},this.#A=EN(t)}async#g(){this.#s!==Zi&&(this.#s=zi,this.dispatchEvent(new Event("error")),await CN(this.#c.reconnectionTime),this.#s===zi&&(this.#c.lastEventId.length&&this.#r.headersList.set("last-event-id",this.#c.lastEventId,!0),this.#l()))}close(){Pt.brandCheck(this,e),this.#s!==Zi&&(this.#s=Zi,this.#A.abort(),this.#r=null)}get onopen(){return this.#e.open}set onopen(t){this.#e.open&&this.removeEventListener("open",this.#e.open),typeof t=="function"?(this.#e.open=t,this.addEventListener("open",t)):this.#e.open=null}get onmessage(){return this.#e.message}set onmessage(t){this.#e.message&&this.removeEventListener("message",this.#e.message),typeof t=="function"?(this.#e.message=t,this.addEventListener("message",t)):this.#e.message=null}get onerror(){return this.#e.error}set onerror(t){this.#e.error&&this.removeEventListener("error",this.#e.error),typeof t=="function"?(this.#e.error=t,this.addEventListener("error",t)):this.#e.error=null}},YB={CONNECTING:{__proto__:null,configurable:!1,enumerable:!0,value:zi,writable:!1},OPEN:{__proto__:null,configurable:!1,enumerable:!0,value:_B,writable:!1},CLOSED:{__proto__:null,configurable:!1,enumerable:!0,value:Zi,writable:!1}};Object.defineProperties(Hr,YB);Object.defineProperties(Hr.prototype,YB);Object.defineProperties(Hr.prototype,{close:Js,onerror:Js,onmessage:Js,onopen:Js,readyState:Js,url:Js,withCredentials:Js});Pt.converters.EventSourceInitDict=Pt.dictionaryConverter([{key:"withCredentials",converter:Pt.converters.boolean,defaultValue:()=>!1},{key:"dispatcher",converter:Pt.converters.any}]);OB.exports={EventSource:Hr,defaultReconnectionTime:LB}});var da=Q((JY,R)=>{"use strict";var bN=fr(),PB=ei(),yN=Qr(),xN=_d(),vN=Br(),kN=qc(),DN=cE(),RN=dE(),HB=L(),ha=U(),{InvalidArgumentError:ga}=HB,Vr=tm(),TN=si(),FN=vl(),SN=Mm(),UN=Rl(),NN=hl(),GN=En(),{getGlobalDispatcher:VB,setGlobalDispatcher:MN}=xn(),LN=vn(),_N=on(),YN=nn();Object.assign(PB.prototype,Vr);R.exports.Dispatcher=PB;R.exports.Client=bN;R.exports.Pool=yN;R.exports.BalancedPool=xN;R.exports.Agent=vN;R.exports.ProxyAgent=kN;R.exports.EnvHttpProxyAgent=DN;R.exports.RetryAgent=RN;R.exports.RetryHandler=GN;R.exports.DecoratorHandler=LN;R.exports.Redire Line truncated
`+e.errors.map(t=>` - ${t.message}`).join(`
`)}var EM=class extends Error{constructor(e,t,s){super(dM(s)),this.request=e,this.headers=t,this.response=s,this.errors=s.errors,this.data=s.data,Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}name="GraphqlResponseError";errors;data},mM=["method","baseUrl","url","headers","request","query","mediaType","operationName"],fM=["query","method","url"],GC=/\/api\/v3\/?$/;function QM(e,t,s){if(s){if(typeof t=="string"&&"query"in s)return Promise.reject(new Error('[@octokit/graphql] "query" cannot be used as variable name'));for(let n in s)if(fM.includes(n))return Promise.reject(new Error(`[@octokit/graphql] "${n}" cannot be used as variable name`))}let r=typeof t=="string"?Object.assign({query:t},s):t,i=Object.keys(r).reduce((n,a)=>mM.includes(a)?(n[a]=r[a],n):(n.variables||(n.variables={}),n.variables[a]=r[a],n),{}),o=r.baseUrl||e.endpoint.DEFAULTS.baseUrl;return GC.test(o)&&(i.url=o.replace(GC,"/api/graphql")),e(i).then(n=>{if(n.data.errors){let a={};for(let A of Object.keys(n.headers))a[A]=n.headers[A];throw new EM(i,a,n.data)}return n.data.data})}function Nu(e,t){let s=e.defaults(t);return Object.assign((i,o)=>QM(s,i,o),{defaults:Nu.bind(null,s),endpoint:s.endpoint})}var tJ=Nu(ro,{headers:{"user-agent":`octokit-graphql.js/${hM} ${ds()}`},method:"POST",url:"/graphql"});function MC(e){return Nu(e,{method:"POST",url:"/graphql"})}var Gu="(?:[a-zA-Z0-9_-]+)",LC="\\.",_C=new RegExp(`^${Gu}${LC}${Gu}${LC}${Gu}$`),BM=_C.test.bind(_C);async function CM(e){let t=BM(e),s=e.startsWith("v1.")||e.startsWith("ghs_"),r=e.startsWith("ghu_");return{type:"token",token:e,tokenType:t?"app":s?"installation":r?"user-to-server":"oauth"}}function IM(e){return e.split(/\./).length===3?`bearer ${e}`:`token ${e}`}async function wM(e,t,s,r){let i=t.endpoint.merge(s,r);return i.headers.authorization=IM(e),t(i)}var YC=function(t){if(!t)throw new Error("[@octokit/auth-token] No token passed to createTokenAuth");if(typeof t!="string")throw new Error("[@octokit/auth-token] Token passed to createTokenAuth is not a string");return t=t.replace(/^(token|bearer) +/i,""),Object.assign(CM.bind(null,t),{hook:wM.bind(null,t)})};var Mu="7.0.6";var OC=()=>{},bM=console.warn.bind(console),yM=console.error.bind(console);function xM(e={}){return typeof e.debug!="function"&&(e.debug=OC),typeof e.info!="function"&&(e.info=OC),typeof e.warn!="function"&&(e.warn=bM),typeof e.error!="function"&&(e.error=yM),e}var JC=`octokit-core.js/${Mu} ${ds()}`,Da=class{static VERSION=Mu;static defaults(t){return class extends this{constructor(...r){let i=r[0]||{};if(typeof t=="function"){super(t(i));return}super(Object.assign({},t,i,i.userAgent&&t.userAgent?{userAgent:`${i.userAgent} ${t.userAgent}`}:null))}}}static plugins=[];static plugin(...t){let s=this.plugins;return class extends this{static plugins=s.concat(t.filter(i=>!s.includes(i)))}}constructor(t={}){let s=new dC.Collection,r={baseUrl:ro.endpoint.DEFAULTS.baseUrl,headers:{},request:Object.assign({},t.request,{hook:s.bind(null,"request")}),mediaType:{previews:[],format:""}};if(r.headers["user-agent"]=t.userAgent?`${t.userAgent} ${JC}`:JC,t.baseUrl&&(r.baseUrl=t.baseUrl),t.previews&&(r.mediaType.previews=t.previews),t.timeZone&&(r.headers["time-zone"]=t.timeZone),this.request=ro.defaults(r),this.graphql=MC(this.request).defaults(r),this.log=xM(t.log),this.hook=s,t.authStrategy){let{authStrategy:o,...n}=t,a=o(Object.assign({request:this.request,log:this.log,octokit:this,octokitOptions:n},t.auth));s.wrap("request",a.hook),this.auth=a}else if(!t.auth)this.auth=async()=>({type:"unauthenticated"});else{let o=YC(t.auth);s.wrap("request",o.hook),this.auth=o}let i=this.constructor;for(let o=0;o<i.plugins.length;++o)Object.assign(this,i.plugins[o](this,t))}request;graphql;log;hook;auth};var Lu="17.0.0";var vM={actions:{addCustomLabelsToSelfHostedRunnerForOrg:["POST /orgs/{org}/actions/runners/{runner_id}/labels"],addCustomLabelsToSelfHostedRunnerForRepo:["POST /repos/{owner}/{repo}/actions/runners/{runner_id}/labels"],addRepoAccessToSelfHostedRunnerGroupInOrg:["PUT /orgs/{org}/actions/runner-groups/{runner_group_id}/repositories/{repository_id}"],addSelectedRepoToOrgSecret:["PUT /orgs/{org}/actions/secrets/{secret_name}/repositories/{repository_id}"],addSelectedRepoToOrgVariable:["PUT /orgs/{org}/actions/variables/{name}/repositories/{repository_id}"],approveWorkflowRun:["POST /repos/{owner}/{repo}/actions/runs/{run_id}/approve"],cancelWorkflowRun:["POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel"],createEnvironmentVariable:["POST /repos/{owner}/{repo}/environments/{environment_name}/variables"],createHostedRunnerForOrg:["POST /orgs/{org}/actions/hosted-runners"],createOrUpdateEnvironmentSecret:["PUT /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}"],createOrUpdateOrgSecret:["PUT /orgs/{org}/actions/secrets/{secret_name}"],createOrUpdateRepoSecret:["PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}"],createOrgVariable:["POST /orgs/{org}/actions/variables"],cre Line truncated
globstar while`,e,l,t,p,g),this.matchOne(e.slice(l),t.slice(p),s))return this.debug("globstar found match!",l,a,g),!0;if(g==="."||g===".."||!r.dot&&g.charAt(0)==="."){this.debug("dot detected!",e,l,t,p);break}this.debug("globstar swallow a segment, and continue"),l++}return!!(s&&(this.debug(`
>>> no match, partial?`,e,l,t,p),l===a))}let h;if(typeof c=="string"?(h=u===c,this.debug("string match",c,u,h)):(h=c.test(u),this.debug("pattern match",c,u,h)),!h)return!1}if(o===a&&n===A)return!0;if(o===a)return s;if(n===A)return o===a-1&&e[o]==="";throw new Error("wtf?")}braceExpand(){return OI(this.pattern,this.options)}parse(e){Ya(e);let t=this.options;if(e==="**")return be;if(e==="")return"";let s,r=null;(s=e.match(xL))?r=t.dot?kL:vL:(s=e.match(EL))?r=(t.nocase?t.dot?BL:QL:t.dot?fL:mL)(s[1]):(s=e.match(DL))?r=(t.nocase?t.dot?TL:RL:t.dot?FL:SL)(s):(s=e.match(CL))?r=t.dot?wL:IL:(s=e.match(bL))&&(r=yL);let i=GI.fromGlob(e,this.options).toMMPattern();return r&&typeof i=="object"&&Reflect.defineProperty(i,"test",{value:r}),i}makeRe(){if(this.regexp||this.regexp===!1)return this.regexp;let e=this.set;if(!e.length)return this.regexp=!1,this.regexp;let t=this.options,s=t.noglobstar?GL:t.dot?ML:LL,r=new Set(t.nocase?["i"]:[]),i=e.map(a=>{let A=a.map(u=>{if(u instanceof RegExp)for(let l of u.flags.split(""))r.add(l);return typeof u=="string"?PL(u):u===be?be:u._src});A.forEach((u,l)=>{let p=A[l+1],g=A[l-1];u!==be||g===be||(g===void 0?p!==void 0&&p!==be?A[l+1]="(?:\\/|"+s+"\\/)?"+p:A[l]=s:p===void 0?A[l-1]=g+"(?:\\/|\\/"+s+")?":p!==be&&(A[l-1]=g+"(?:\\/|\\/"+s+"\\/)"+p,A[l+1]=be))});let c=A.filter(u=>u!==be);if(this.partial&&c.length>=1){let u=[];for(let l=1;l<=c.length;l++)u.push(c.slice(0,l).join("/"));return"(?:"+u.join("|")+")"}return c.join("/")}).join("|"),[o,n]=e.length>1?["(?:",")"]:["",""];i="^"+o+i+n+"$",this.partial&&(i="^(?:\\/|"+o+i.slice(1,-1)+n+")$"),this.negate&&(i="^(?!"+i+").+$");try{this.regexp=new RegExp(i,[...r].join(""))}catch{this.regexp=!1}return this.regexp}slashSplit(e){return this.preserveMultipleSlashes?e.split("/"):this.isWindows&&/^\/\/[^\/]+/.test(e)?["",...e.split(/\/+/)]:e.split(/\/+/)}match(e,t=this.partial){if(this.debug("match",e,this.pattern),this.comment)return!1;if(this.empty)return e==="";if(e==="/"&&t)return!0;let s=this.options;this.isWindows&&(e=e.split("\\").join("/"));let r=this.slashSplit(e);this.debug(this.pattern,"split",r);let i=this.set;this.debug(this.pattern,"set",i);let o=r[r.length-1];if(!o)for(let n=r.length-2;!o&&n>=0;n--)o=r[n];for(let n=0;n<i.length;n++){let a=i[n],A=r;if(s.matchBase&&a.length===1&&(A=[o]),this.matchOne(A,a,t))return s.flipNegate?!0:!this.negate}return s.flipNegate?!1:this.negate}static defaults(e){return ye.defaults(e).Minimatch}};ye.AST=GI;ye.Minimatch=fs;ye.escape=MI;ye.unescape=Kr;var HL=typeof performance=="object"&&performance&&typeof performance.now=="function"?performance:Date,PI=new Set,$u=typeof process=="object"&&process?process:{},HI=(e,t,s,r)=>{typeof $u.emitWarning=="function"?$u.emitWarning(e,t,s,r):console.error(`[${s}] ${t}: ${e}`)},Oa=globalThis.AbortController,hI=globalThis.AbortSignal;if(typeof Oa>"u"){hI=class{onabort;_onabort=[];reason;aborted=!1;addEventListener(s,r){this._onabort.push(r)}},Oa=class{constructor(){t()}signal=new hI;abort(s){if(!this.signal.aborted){this.signal.reason=s,this.signal.aborted=!0;for(let r of this.signal._onabort)r(s);this.signal.onabort?.(s)}}};let e=$u.env?.LRU_CACHE_IGNORE_AC_WARNING!=="1",t=()=>{e&&(e=!1,HI("AbortController is not defined. If using lru-cache in node 14, load an AbortController polyfill from the `node-abort-controller` package. A minimal polyfill is provided for use by LRUCache.fetch(), but it should not be relied upon in other contexts (eg, passing it to other APIs that use AbortController/AbortSignal might have undesirable effects). You may disable this with LRU_CACHE_IGNORE_AC_WARNING=1 in the env.","NO_ABORT_CONTROLLER","ENOTSUP",t))}}var VL=e=>!PI.has(e),ms=e=>e&&e===Math.floor(e)&&e>0&&isFinite(e),VI=e=>ms(e)?e<=Math.pow(2,8)?Uint8Array:e<=Math.pow(2,16)?Uint16Array:e<=Math.pow(2,32)?Uint32Array:e<=Number.MAX_SAFE_INTEGER?La:null:null,La=class extends Array{constructor(e){super(e),this.fill(0)}},qL=class uo{heap;length;static#e=!1;static create(t){let s=VI(t);if(!s)return[];uo.#e=!0;let r=new uo(t,s);return uo.#e=!1,r}constructor(t,s){if(!uo.#e)throw new TypeError("instantiate Stack using Stack.create(n)");this.heap=new s(t),this.length=0}push(t){this.heap[this.length++]=t}pop(){return this.heap[--this.length]}},Pa=class qI{#e;#t;#i;#s;#r;#A;#a;#c;get perf(){return this.#c}ttl;ttlResolution;ttlAutopurge;updateAgeOnGet;updateAgeOnHas;allowStale;noDisposeOnSet;noUpdateTTL;maxEntrySize;sizeCalculation;noDeleteOnFetchRejection;noDeleteOnStaleGet;allowStaleOnFetchAbort;allowStaleOnFetchRejection;ignoreFetchAbort;#l;#g;#h;#u;#n;#d;#C;#B;#E;#k;#m;#b;#y;#f;#Q;#I;#x;#o;#U;static unsafeExposeInternals(t){return{starts:t.#y,ttls:t.#f,autopurgeTimers:t.#Q,sizes:t.#b,keyMap:t.#h,keyList:t.#u,valList:t.#n,next:t.#d,prev:t.#C,get head(){return t.#B},get tail(){return t.#E},free:t.#k,isBackgroundFetch:s=>t.#p(s),backgroundFetch:(s,r,i,o)=>t.#J(s,r,i,o),moveToTail:s=>t.#G(s),indexes:s=>t.#R(s),rindexes:s=>t.#T(s),isStale:s=>t.#w(s)}}get max(){return this.#e}get maxSize(){return th Line truncated
${s.data.body}`:t.body=s.data.body}return t.body=t.body?this.truncateReleaseNotes(t.body):void 0,this.github.rest.repos.createRelease(t)}async updateRelease(t){if(typeof t.make_latest=="string"&&!["true","false","legacy"].includes(t.make_latest)&&(t.make_latest=void 0),t.generate_release_notes){let s=await this.getReleaseNotes(t);t.generate_release_notes=!1,t.body?t.body=`${t.body}
${s.data.body}`:t.body=s.data.body}return t.body=t.body?this.truncateReleaseNotes(t.body):void 0,this.github.rest.repos.updateRelease(t)}async finalizeRelease(t){return await this.github.rest.repos.updateRelease({owner:t.owner,repo:t.repo,release_id:t.release_id,draft:!1,make_latest:t.make_latest})}allReleases(t){let s={per_page:100,...t};return this.github.paginate.iterator(this.github.rest.repos.listReleases.endpoint.merge(s))}async listReleaseAssets(t){return this.github.paginate(this.github.rest.repos.listReleaseAssets,{...t,per_page:100})}async deleteReleaseAsset(t){await this.github.rest.repos.deleteReleaseAsset(t)}async deleteRelease(t){await this.github.rest.repos.deleteRelease(t)}async updateReleaseAsset(t){return await this.github.rest.repos.updateReleaseAsset(t)}async uploadReleaseAsset(t){return this.github.request({method:"POST",url:t.url,headers:{"content-length":`${t.size}`,"content-type":t.mime,authorization:`token ${t.token}`},data:t.data})}},k_=e=>({name:(0,xw.basename)(e),mime:D_(e),size:(0,ww.statSync)(e).size}),D_=e=>(0,yw.lookup)(e)||"application/octet-stream",vw=async(e,t,s,r,i)=>{let[o,n]=e.github_repository.split("/"),{name:a,mime:A,size:c}=k_(r),u=s.match(/\/releases\/(\d+)\/assets/),l=u?Number(u[1]):void 0,p=i.find(({name:E,label:m})=>E===a||E===ap(a)||m===a);if(p){if(e.input_overwrite_files===!1)return console.log(`Asset ${a} already exists and overwrite_files is false...`),null;console.log(`\u267B\uFE0F Deleting previously uploaded asset ${a}...`),await t.deleteReleaseAsset({asset_id:p.id||1,owner:o,repo:n})}console.log(`\u2B06\uFE0F Uploading ${a}...`);let g=new URL(s);g.searchParams.append("name",a);let h=async()=>{let E=await(0,bw.open)(r);try{return await t.uploadReleaseAsset({url:g.toString(),size:c,mime:A,token:e.github_token,data:E.readableWebStream({type:"bytes"})})}finally{await E.close()}};try{let E=await h(),m=E.data;if(E.status!==201)throw new Error(`Failed to upload release asset ${a}. received status code ${E.status}
${m.message}
${JSON.stringify(m.errors)}`);if(m.name&&m.name!==a&&m.id){console.log(`\u270F\uFE0F Restoring asset label to ${a}...`);try{let{data:d}=await t.updateReleaseAsset({owner:o,repo:n,asset_id:m.id,name:m.name,label:a});return console.log(`\u2705 Uploaded ${a}`),d}catch(d){console.warn(`error updating release asset label for ${a}: ${d}`)}}return console.log(`\u2705 Uploaded ${a}`),m}catch(E){let m=E?.status??E?.response?.status,d=E?.response?.data;if(e.input_overwrite_files!==!1&&m===422&&d?.errors?.[0]?.code==="already_exists"&&l!==void 0){console.log(`\u26A0\uFE0F Asset ${a} already exists (race condition), refreshing assets and retrying once...`);let C=(await t.listReleaseAssets({owner:o,repo:n,release_id:l})).find(({name:B})=>B==ap(a));if(C){await t.deleteReleaseAsset({owner:o,repo:n,asset_id:C.id});let B=await h(),b=B.data;if(B.status!==201)throw new Error(`Failed to upload release asset ${a}. received status code ${B.status}
${b.message}
${JSON.stringify(b.errors)}`);return console.log(`\u2705 Uploaded ${a}`),b}}throw E}},Ap=async(e,t,s=3)=>{if(s<=0)throw console.log("\u274C Too many retries. Aborting..."),new Error("Too many retries.");let[r,i]=e.github_repository.split("/"),o=e.input_tag_name||(ja(e.github_ref)?e.github_ref.replace("refs/tags/",""):""),n=e.input_discussion_category_name,a=e.input_generate_release_notes;try{let A=await kw(t,r,i,o);if(A===void 0)return await Iw(o,e,t,r,i,n,a,s);let c=A;console.log(`Found release ${c.name} (with id=${c.id})`);let u=c.id,l;e.input_target_commitish&&e.input_target_commitish!==c.target_commitish?(console.log(`Updating commit from "${c.target_commitish}" to "${e.input_target_commitish}"`),l=e.input_target_commitish):l=c.target_commitish;let p=o,g=e.input_name||c.name||o,h=np(e)||"",E=c.body||"",m;e.input_append_body&&h&&E?m=E+`
`+h:m=h||E;let d=e.input_prerelease!==void 0?e.input_prerelease:c.prerelease,f=e.input_make_latest;return{release:(await t.updateRelease({owner:r,repo:i,release_id:u,tag_name:p,target_commitish:l,name:g,body:m,draft:c.draft,prerelease:d,discussion_category_name:n,generate_release_notes:a,make_latest:f})).data,created:!1}}catch(A){if(A.status!==404)throw console.log(`\u26A0\uFE0F Unexpected error fetching GitHub release for tag ${e.github_ref}: ${A}`),A;return await Iw(o,e,t,r,i,n,a,s)}},cp=async(e,t,s,r=!1,i=3)=>{if(e.input_draft===!0||s.draft===!1)return s;if(i<=0)throw console.log("\u274C Too many retries. Aborting..."),new Error("Too many retries.");let[o,n]=e.github_repository.split("/");try{let{data:a}=await t.finalizeRelease({owner:o,repo:n,release_id:s.id,make_latest:e.input_make_latest});return a}catch(a){if(console.warn(`error finalizing release: ${a}`),r&&s.draft&&M_(a)){let A=!1;try{console.log(`\u{1F9F9} Deleting draft release ${s.id} for tag ${s.tag_name} because tag creation is blocked by repository rules...`),await t.deleteRelease({owner:o,repo:n,release_id:s.id}),A=!0}catch(u){console.warn(`error deleting orphan draft release ${s.id}: ${u}`)}let c=A?`Deleted draft release ${s.id} to avoid leaving an orphaned draft release.`:`Failed to delete draft release ${s.id}; manual cleanup may still be required.`;throw new Error(`Tag creation for ${s.tag_name} is blocked by repository rules. ${c}`)}return console.log(`retrying... (${i-1} retries remaining)`),cp(e,t,s,r,i-1)}},lp=async(e,t,s,r=3)=>{if(r<=0)throw console.log("\u274C Too many retries. Aborting..."),new Error("Too many retries.");let[i,o]=e.github_repository.split("/");try{return await t.listReleaseAssets({owner:i,repo:o,release_id:s.id})}catch(n){return console.warn(`error listing assets of release: ${n}`),console.log(`retrying... (${r-1} retries remaining)`),lp(e,t,s,r-1)}};async function kw(e,t,s,r){try{let{data:i}=await e.getReleaseByTag({owner:t,repo:s,tag:r});return i}catch(i){if(i.status===404)return;throw i}}var R_=1e3,T_=2;async function F_(e){await new Promise(t=>setTimeout(t,e))}async function S_(e,t,s,r){let i=[],o=0;for await(let n of e.allReleases({owner:t,repo:s}))if(i.push(...n.data.filter(a=>a.tag_name===r)),o+=1,o>=T_)break;return i}function U_(e,t){return t&&e.some(s=>s.id===t.id)||e.length===0?t:[...e].sort((s,r)=>s.draft!==r.draft?Number(s.draft)-Number(r.draft):s.id-r.id)[0]}async function N_(e,t,s,r,i,o){for(let n of o)if(!(n.id===i||!n.draft||n.assets.length>0))try{console.log(`\u{1F9F9} Removing duplicate draft release ${n.id} for tag ${r}...`),await e.deleteRelease({owner:t,repo:s,release_id:n.id})}catch(a){console.warn(`error deleting duplicate release ${n.id}: ${a}`)}}async function G_(e,t,s,r,i,o){let n=Math.max(o,1);for(let a=1;a<=n;a+=1){let A;try{A=await kw(e,t,s,r)}catch(l){console.warn(`error reloading release for tag ${r}: ${l}`)}let c=[];try{c=await S_(e,t,s,r)}catch(l){console.warn(`error listing recent releases for tag ${r}: ${l}`)}let u=U_(c,A);if(u)return u.id!==i.id&&console.log(`\u21AA\uFE0F Using release ${u.id} for tag ${r} instead of duplicate draft ${i.id}`),await N_(e,t,s,r,u.id,c),u;a<n&&(console.log(`Release ${i.id} is not yet discoverable by tag ${r}, retrying... (${n-a} retries remaining)`),await F_(R_))}return console.log(`\u26A0\uFE0F Continuing with newly created release ${i.id} because tag ${r} is still not discoverable`),i}async function Iw(e,t,s,r,i,o,n,a){let A=e,c=t.input_name||e,u=np(t),l=t.input_prerelease,p=l===!0?t.input_draft===!0:!0,g=t.input_target_commitish,h=t.input_make_latest,E="";g&&(E=` using commit "${g}"`),console.log(`\u{1F469}\u200D\u{1F3ED} Creating new GitHub release for tag ${A}${E}...`);try{let m=await s.createRelease({owner:r,repo:i,tag_name:A,name:c,body:u,draft:p,prerelease:l,target_commitish:g,discussion_category_name:o,generate_release_notes:n,make_latest:h}),d=await G_(s,r,i,A,m.data,a);return{release:d,created:d.id===m.data.id}}catch(m){switch(console.log(`\u26A0\uFE0F GitHub release failed with status: ${m.status}`),console.log(`${JSON.stringify(m.response.data)}`),m.status){case 403:throw console.log("Skip retry \u2014 your GitHub token/PAT does not have the required permission to create a release"),m;case 404:throw console.log("Skip retry - discussion category mismatch"),m;case 422:if(m.response?.data?.errors?.[0]?.code==="already_exists")console.log("\u26A0\uFE0F Release already exists (race condition detected), retrying to find and update existing release...");else throw console.log("Skip retry - validation failed"),m;break}return console.log(`retrying... (${a-1} retries remaining)`),Ap(t,s,a-1)}}function M_(e){let t=e?.response?.data?.errors;return!Array.isArray(t)||e?.status!==422?!1:t.some(({field:s,message:r})=>s==="pre_receive"&&typeof r=="string"&&r.includes("creations being restricted"))}var Dw=require("process");async function L_(){try{let e=Qw(Dw.env);if(!e.input_tag_name&&!ja(e.github_ref)&&!e.input_draft)throw new Error("\u26A0\uFE0F Git Line truncated
/*! Bundled license information:
undici/lib/web/fetch/body.js:
(*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> *)
undici/lib/web/websocket/frame.js:
(*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> *)
mime-db/index.js:
(*!
* mime-db
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2015-2022 Douglas Christopher Wilson
* MIT Licensed
*)
mime-types/index.js:
(*!
* mime-types
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*)
@octokit/request-error/dist-src/index.js:
(* v8 ignore else -- @preserve -- Bug with vitest coverage where it sees an else branch that doesn't exist *)
@octokit/request/dist-bundle/index.js:
(* v8 ignore next -- @preserve *)
(* v8 ignore else -- @preserve *)
*/
module.exports=function(e,t){"use strict";var r={};function __webpack_require__(t){if(r[t]){return r[t].exports}var i=r[t]={i:t,l:false,exports:{}};e[t].call(i.exports,i,i.exports,__webpack_require__);i.l=true;return i.exports}__webpack_require__.ab=__dirname+"/";function startup(){return __webpack_require__(131)}return startup()}({3:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:true});function isObject(e){return Object.prototype.toString.call(e)==="[object Object]"}function isPlainObject(e){var t,r;if(isObject(e)===false)return false;t=e.constructor;if(t===undefined)return true;r=t.prototype;if(isObject(r)===false)return false;if(r.hasOwnProperty("isPrototypeOf")===false){return false}return true}t.isPlainObject=isPlainObject},4:function(e,t){"use strict";var r="\ufeff";t.PrependBOM=PrependBOMWrapper;function PrependBOMWrapper(e,t){this.encoder=e;this.addBOM=true}PrependBOMWrapper.prototype.write=function(e){if(this.addBOM){e=r+e;this.addBOM=false}return this.encoder.write(e)};PrependBOMWrapper.prototype.end=function(){return this.encoder.end()};t.StripBOM=StripBOMWrapper;function StripBOMWrapper(e,t){this.decoder=e;this.pass=false;this.options=t||{}}StripBOMWrapper.prototype.write=function(e){var t=this.decoder.write(e);if(this.pass||!t)return t;if(t[0]===r){t=t.slice(1);if(typeof this.options.stripBOM==="function")this.options.stripBOM()}this.pass=true;return t};StripBOMWrapper.prototype.end=function(){return this.decoder.end()}},11:function(e){e.exports=wrappy;function wrappy(e,t){if(e&&t)return wrappy(e)(t);if(typeof e!=="function")throw new TypeError("need wrapper function");Object.keys(e).forEach(function(t){wrapper[t]=e[t]});return wrapper;function wrapper(){var t=new Array(arguments.length);for(var r=0;r<t.length;r++){t[r]=arguments[r]}var i=e.apply(this,t);var n=t[t.length-1];if(typeof i==="function"&&i!==n){Object.keys(n).forEach(function(e){i[e]=n[e]})}return i}}},16:function(e){e.exports=require("tls")},21:function(e){"use strict";e.exports={437:"cp437",737:"cp737",775:"cp775",850:"cp850",852:"cp852",855:"cp855",856:"cp856",857:"cp857",858:"cp858",860:"cp860",861:"cp861",862:"cp862",863:"cp863",864:"cp864",865:"cp865",866:"cp866",869:"cp869",874:"windows874",922:"cp922",1046:"cp1046",1124:"cp1124",1125:"cp1125",1129:"cp1129",1133:"cp1133",1161:"cp1161",1162:"cp1162",1163:"cp1163",1250:"windows1250",1251:"windows1251",1252:"windows1252",1253:"windows1253",1254:"windows1254",1255:"windows1255",1256:"windows1256",1257:"windows1257",1258:"windows1258",28591:"iso88591",28592:"iso88592",28593:"iso88593",28594:"iso88594",28595:"iso88595",28596:"iso88596",28597:"iso88597",28598:"iso88598",28599:"iso88599",28600:"iso885910",28601:"iso885911",28603:"iso885913",28604:"iso885914",28605:"iso885915",28606:"iso885916",windows874:{type:"_sbcs",chars:"€����…�����������‘’“”•–—�������� กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู����฿เแโใไๅๆ็่้๊๋์ํ๎๏๐๑๒๓๔๕๖๗๘๙๚๛����"},win874:"windows874",cp874:"windows874",windows1250:{type:"_sbcs",chars:"€�‚�„…†‡�‰Š‹ŚŤŽŹ�‘’“”•–—�™š›śťžź ˇ˘Ł¤Ą¦§¨©Ş«¬­®Ż°±˛ł´µ¶·¸ąş»Ľ˝ľżŔÁÂĂÄĹĆÇČÉĘËĚÍÎĎĐŃŇÓÔŐÖ×ŘŮÚŰÜÝŢßŕáâăäĺćçčéęëěíîďđńňóôőö÷řůúűüýţ˙"},win1250:"windows1250",cp1250:"windows1250",windows1251:{type:"_sbcs",chars:"ЂЃ‚ѓ„…†‡€‰Љ‹ЊЌЋЏђ‘’“”•–—�™љ›њќћџ ЎўЈ¤Ґ¦§Ё©Є«¬­®Ї°±Ііґµ¶·ё№є»јЅѕїАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя"},win1251:"windows1251",cp1251:"windows1251",windows1252:{type:"_sbcs",chars:"€�‚ƒ„…†‡ˆ‰Š‹Œ�Ž��‘’“”•–—˜™š›œ�žŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ"},win1252:"windows1252",cp1252:"windows1252",windows1253:{type:"_sbcs",chars:"€�‚ƒ„…†‡�‰�‹�����‘’“”•–—�™�›���� ΅Ά£¤¥¦§¨©�«¬­®―°±²³΄µ¶·ΈΉΊ»Ό½ΎΏΐΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡ�ΣΤΥΦΧΨΩΪΫάέήίΰαβγδεζηθικλμνξοπρςστυφχψωϊϋόύώ�"},win1253:"windows1253",cp1253:"windows1253",windows1254:{type:"_sbcs",chars:"€�‚ƒ„…†‡ˆ‰Š‹Œ����‘’“”•–—˜™š›œ��Ÿ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏĞÑÒÓÔÕÖרÙÚÛÜİŞßàáâãäåæçèéêëìíîïğñòóôõö÷øùúûüışÿLine truncated
+11
View File
@@ -0,0 +1,11 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}
+5084 -2561
View File
File diff suppressed because it is too large. Load diff
+25 -24
View File
@@ -1,44 +1,45 @@
{
"name": "action-gh-release",
"version": "2.5.2",
"version": "0.1.10",
"private": true,
"description": "GitHub Action for creating GitHub Releases",
"main": "lib/main.js",
"scripts": {
"build": "esbuild src/main.ts --bundle --platform=node --format=cjs --target=node20 --outfile=dist/index.js --minify",
"build-debug": "esbuild src/main.ts --bundle --platform=node --format=cjs --target=node20 --outfile=dist/index.js --sourcemap --keep-names",
"typecheck": "tsc --noEmit",
"test": "vitest --coverage",
"build": "ncc build src/main.ts --minify",
"test": "jest",
"fmt": "prettier --write \"src/**/*.ts\" \"__tests__/**/*.ts\"",
"fmtcheck": "prettier --check \"src/**/*.ts\" \"__tests__/**/*.ts\"",
"updatetag": "git tag -d v2 && git push origin :v2 && git tag -a v2 -m '' && git push origin v2"
"fmtcheck": "prettier --check \"src/**/*.ts\" \"__tests__/**/*.ts\""
},
"repository": {
"type": "git",
"url": "git+https://github.com/softprops/action-gh-release.git"
"url": "git+https://github.com/softprops/action-gh-template.git"
},
"keywords": [
"actions"
],
"author": "softprops",
"license": "MIT",
"dependencies": {
"@actions/core": "^3.0.0",
"@actions/github": "^9.0.0",
"@octokit/plugin-retry": "^8.1.0",
"@octokit/plugin-throttling": "^11.0.3",
"glob": "^13.0.6",
"mime-types": "^3.0.2"
"@actions/core": "^1.4.0",
"@actions/github": "^5.0.0",
"@octokit/plugin-retry": "^3.0.9",
"@octokit/plugin-throttling": "^3.5.1",
"glob": "^7.1.6",
"mime": "^2.4.4",
"node-fetch": "^2.6.1"
},
"devDependencies": {
"@types/glob": "^9.0.0",
"@types/mime-types": "^3.0.1",
"@types/node": "^20.19.37",
"@vitest/coverage-v8": "^4.1.0",
"esbuild": "^0.27.3",
"prettier": "3.8.1",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"typescript-formatter": "^7.2.2",
"vitest": "^4.0.4"
"@types/glob": "^7.1.1",
"@types/jest": "^24.0.25",
"@types/mime": "^2.0.1",
"@types/node": "^12.12.24",
"@types/node-fetch": "^2.5.12",
"@zeit/ncc": "^0.21.0",
"jest": "^24.9.0",
"jest-circus": "^24.9.0",
"prettier": "1.19.1",
"ts-jest": "^24.2.0",
"typescript": "^3.7.4",
"typescript-formatter": "^7.2.2"
}
}
Executable
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# actions requires a node_modules dir https://github.com/actions/toolkit/blob/master/docs/javascript-action.md#publish-a-releasesv1-action
# but its recommended not to check these in https://github.com/actions/toolkit/blob/master/docs/action-versioning.md#recommendations
# as such the following hack is how we dill with it
if [[ $# -ne 1 ]]; then
echo "please pass a release version. i.e. $0 v1"
exit 1
fi
git checkout -b releases/$1 # If this branch already exists, omit the -b flag
rm -rf node_modules
sed -i '/node_modules/d' .gitignore # Bash command that removes node_modules from .gitignore
npm install --production
git add node_modules -f .gitignore
git commit -m node_modules
git push origin releases/$1
+136 -715
View File
@@ -1,9 +1,9 @@
import { GitHub } from '@actions/github/lib/utils';
import { statSync } from 'fs';
import { open } from 'fs/promises';
import { lookup } from 'mime-types';
import { basename } from 'path';
import { alignAssetName, Config, isTag, releaseBody } from './util';
import fetch from "node-fetch";
import { GitHub } from "@actions/github/lib/utils";
import { Config, isTag, releaseBody } from "./util";
import { lstatSync, readFileSync } from "fs";
import { getType } from "mime";
import { basename } from "path";
type GitHub = InstanceType<typeof GitHub>;
@@ -11,6 +11,7 @@ export interface ReleaseAsset {
name: string;
mime: string;
size: number;
data: Buffer;
}
export interface Release {
@@ -23,16 +24,15 @@ export interface Release {
target_commitish: string;
draft: boolean;
prerelease: boolean;
assets: Array<{ id: number; name: string; label?: string | null }>;
}
export interface ReleaseResult {
release: Release;
created: boolean;
assets: Array<{ id: number; name: string }>;
}
export interface Releaser {
getReleaseByTag(params: { owner: string; repo: string; tag: string }): Promise<{ data: Release }>;
getReleaseByTag(params: {
owner: string;
repo: string;
tag: string;
}): Promise<{ data: Release }>;
createRelease(params: {
owner: string;
@@ -44,8 +44,6 @@ export interface Releaser {
prerelease: boolean | undefined;
target_commitish: string | undefined;
discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
make_latest: 'true' | 'false' | 'legacy' | undefined;
}): Promise<{ data: Release }>;
updateRelease(params: {
@@ -59,44 +57,12 @@ export interface Releaser {
draft: boolean | undefined;
prerelease: boolean | undefined;
discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
make_latest: 'true' | 'false' | 'legacy' | undefined;
}): Promise<{ data: Release }>;
finalizeRelease(params: {
allReleases(params: {
owner: string;
repo: string;
release_id: number;
make_latest: 'true' | 'false' | 'legacy' | undefined;
}): Promise<{ data: Release }>;
allReleases(params: { owner: string; repo: string }): AsyncIterable<{ data: Release[] }>;
listReleaseAssets(params: {
owner: string;
repo: string;
release_id: number;
}): Promise<Array<{ id: number; name: string; label?: string | null; [key: string]: any }>>;
deleteReleaseAsset(params: { owner: string; repo: string; asset_id: number }): Promise<void>;
deleteRelease(params: { owner: string; repo: string; release_id: number }): Promise<void>;
updateReleaseAsset(params: {
owner: string;
repo: string;
asset_id: number;
name: string;
label: string;
}): Promise<{ data: any }>;
uploadReleaseAsset(params: {
url: string;
size: number;
mime: string;
token: string;
data: any;
}): Promise<{ status: number; data: any }>;
}): AsyncIterableIterator<{ data: Release[] }>;
}
export class GitHubReleaser implements Releaser {
@@ -113,27 +79,7 @@ export class GitHubReleaser implements Releaser {
return this.github.rest.repos.getReleaseByTag(params);
}
async getReleaseNotes(params: {
owner: string;
repo: string;
tag_name: string;
target_commitish: string | undefined;
}): Promise<{
data: {
name: string;
body: string;
};
}> {
return await this.github.rest.repos.generateReleaseNotes(params);
}
truncateReleaseNotes(input: string): string {
// release notes can be a maximum of 125000 characters
const githubNotesMaxCharLength = 125000;
return input.substring(0, githubNotesMaxCharLength - 1);
}
async createRelease(params: {
createRelease(params: {
owner: string;
repo: string;
tag_name: string;
@@ -143,29 +89,11 @@ export class GitHubReleaser implements Releaser {
prerelease: boolean | undefined;
target_commitish: string | undefined;
discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
make_latest: 'true' | 'false' | 'legacy' | undefined;
}): Promise<{ data: Release }> {
if (
typeof params.make_latest === 'string' &&
!['true', 'false', 'legacy'].includes(params.make_latest)
) {
params.make_latest = undefined;
}
if (params.generate_release_notes) {
const releaseNotes = await this.getReleaseNotes(params);
params.generate_release_notes = false;
if (params.body) {
params.body = `${params.body}\n\n${releaseNotes.data.body}`;
} else {
params.body = releaseNotes.data.body;
}
}
params.body = params.body ? this.truncateReleaseNotes(params.body) : undefined;
return this.github.rest.repos.createRelease(params);
}
async updateRelease(params: {
updateRelease(params: {
owner: string;
repo: string;
release_id: number;
@@ -176,307 +104,146 @@ export class GitHubReleaser implements Releaser {
draft: boolean | undefined;
prerelease: boolean | undefined;
discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
make_latest: 'true' | 'false' | 'legacy' | undefined;
}): Promise<{ data: Release }> {
if (
typeof params.make_latest === 'string' &&
!['true', 'false', 'legacy'].includes(params.make_latest)
) {
params.make_latest = undefined;
}
if (params.generate_release_notes) {
const releaseNotes = await this.getReleaseNotes(params);
params.generate_release_notes = false;
if (params.body) {
params.body = `${params.body}\n\n${releaseNotes.data.body}`;
} else {
params.body = releaseNotes.data.body;
}
}
params.body = params.body ? this.truncateReleaseNotes(params.body) : undefined;
return this.github.rest.repos.updateRelease(params);
}
async finalizeRelease(params: {
allReleases(params: {
owner: string;
repo: string;
release_id: number;
make_latest: 'true' | 'false' | 'legacy' | undefined;
}) {
return await this.github.rest.repos.updateRelease({
owner: params.owner,
repo: params.repo,
release_id: params.release_id,
draft: false,
make_latest: params.make_latest,
});
}
allReleases(params: { owner: string; repo: string }): AsyncIterable<{ data: Release[] }> {
}): AsyncIterableIterator<{ data: Release[] }> {
const updatedParams = { per_page: 100, ...params };
return this.github.paginate.iterator(
this.github.rest.repos.listReleases.endpoint.merge(updatedParams),
this.github.rest.repos.listReleases.endpoint.merge(updatedParams)
);
}
async listReleaseAssets(params: {
owner: string;
repo: string;
release_id: number;
}): Promise<Array<{ id: number; name: string; label?: string | null; [key: string]: any }>> {
return this.github.paginate(this.github.rest.repos.listReleaseAssets, {
...params,
per_page: 100,
});
}
async deleteReleaseAsset(params: {
owner: string;
repo: string;
asset_id: number;
}): Promise<void> {
await this.github.rest.repos.deleteReleaseAsset(params);
}
async deleteRelease(params: { owner: string; repo: string; release_id: number }): Promise<void> {
await this.github.rest.repos.deleteRelease(params);
}
async updateReleaseAsset(params: {
owner: string;
repo: string;
asset_id: number;
name: string;
label: string;
}): Promise<{ data: any }> {
return await this.github.rest.repos.updateReleaseAsset(params);
}
async uploadReleaseAsset(params: {
url: string;
size: number;
mime: string;
token: string;
data: any;
}): Promise<{ status: number; data: any }> {
return this.github.request({
method: 'POST',
url: params.url,
headers: {
'content-length': `${params.size}`,
'content-type': params.mime,
authorization: `token ${params.token}`,
},
data: params.data,
});
}
}
export const asset = (path: string): ReleaseAsset => {
return {
name: basename(path),
mime: mimeOrDefault(path),
size: statSync(path).size,
size: lstatSync(path).size,
data: readFileSync(path)
};
};
export const mimeOrDefault = (path: string): string => {
return lookup(path) || 'application/octet-stream';
return getType(path) || "application/octet-stream";
};
export const upload = async (
config: Config,
releaser: Releaser,
github: GitHub,
url: string,
path: string,
currentAssets: Array<{ id: number; name: string; label?: string | null }>,
currentAssets: Array<{ id: number; name: string }>
): Promise<any> => {
const [owner, repo] = config.github_repository.split('/');
const { name, mime, size } = asset(path);
const releaseIdMatch = url.match(/\/releases\/(\d+)\/assets/);
const releaseId = releaseIdMatch ? Number(releaseIdMatch[1]) : undefined;
const [owner, repo] = config.github_repository.split("/");
const { name, size, mime, data: body } = asset(path);
const currentAsset = currentAssets.find(
// note: GitHub renames asset filenames that have special characters, non-alphanumeric characters, and leading or trailing periods. The "List release assets" endpoint lists the renamed filenames.
// due to this renaming we need to be mindful when we compare the file name we're uploading with a name github may already have rewritten for logical comparison
// see https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset
({ name: currentName, label: currentLabel }) =>
currentName === name || currentName === alignAssetName(name) || currentLabel === name,
({ name: currentName }) => currentName == name
);
if (currentAsset) {
if (config.input_overwrite_files === false) {
console.log(`Asset ${name} already exists and overwrite_files is false...`);
return null;
} else {
console.log(`♻️ Deleting previously uploaded asset ${name}...`);
await releaser.deleteReleaseAsset({
asset_id: currentAsset.id || 1,
owner,
repo,
});
}
console.log(`♻️ Deleting previously uploaded asset ${name}...`);
await github.rest.repos.deleteReleaseAsset({
asset_id: currentAsset.id || 1,
owner,
repo
});
}
console.log(`⬆️ Uploading ${name}...`);
const endpoint = new URL(url);
endpoint.searchParams.append('name', name);
const uploadAsset = async () => {
const fh = await open(path);
try {
return await releaser.uploadReleaseAsset({
url: endpoint.toString(),
size,
mime,
token: config.github_token,
data: fh.readableWebStream({ type: 'bytes' }),
});
} finally {
await fh.close();
}
};
try {
const resp = await uploadAsset();
const json = resp.data;
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)}`,
);
}
if (json.name && json.name !== name && json.id) {
console.log(`✏️ Restoring asset label to ${name}...`);
try {
const { data } = await releaser.updateReleaseAsset({
owner,
repo,
asset_id: json.id,
name: json.name,
label: name,
});
console.log(`✅ Uploaded ${name}`);
return data;
} catch (error) {
console.warn(`error updating release asset label for ${name}: ${error}`);
}
}
console.log(`✅ Uploaded ${name}`);
return json;
} catch (error: any) {
const errorStatus = error?.status ?? error?.response?.status;
const errorData = error?.response?.data;
// Handle race conditions across concurrent workflows uploading the same asset.
if (
config.input_overwrite_files !== false &&
errorStatus === 422 &&
errorData?.errors?.[0]?.code === 'already_exists' &&
releaseId !== undefined
) {
console.log(
`⚠️ Asset ${name} already exists (race condition), refreshing assets and retrying once...`,
);
const latestAssets = await releaser.listReleaseAssets({
owner,
repo,
release_id: releaseId,
});
const latestAsset = latestAssets.find(
({ name: currentName }) => currentName == alignAssetName(name),
);
if (latestAsset) {
await releaser.deleteReleaseAsset({
owner,
repo,
asset_id: latestAsset.id,
});
const retryResp = await uploadAsset();
const retryJson = retryResp.data;
if (retryResp.status !== 201) {
throw new Error(
`Failed to upload release asset ${name}. received status code ${
retryResp.status
}\n${retryJson.message}\n${JSON.stringify(retryJson.errors)}`,
);
}
console.log(`✅ Uploaded ${name}`);
return retryJson;
}
}
throw error;
endpoint.searchParams.append("name", name);
const resp = await fetch(endpoint, {
headers: {
"content-length": `${size}`,
"content-type": mime,
authorization: `token ${config.github_token}`
},
method: "POST",
body
});
const json = await resp.json();
if (resp.status !== 201) {
throw new Error(
`Failed to upload release asset ${name}. recieved status code ${
resp.status
}\n${json.message}\n${JSON.stringify(json.errors)}`
);
}
return json;
};
export const release = async (
config: Config,
releaser: Releaser,
maxRetries: number = 3,
): Promise<ReleaseResult> => {
maxRetries: number = 3
): Promise<Release> => {
if (maxRetries <= 0) {
console.log(`❌ Too many retries. Aborting...`);
throw new Error('Too many retries.');
throw new Error("Too many retries.");
}
const [owner, repo] = config.github_repository.split('/');
const [owner, repo] = config.github_repository.split("/");
const tag =
config.input_tag_name ||
(isTag(config.github_ref) ? config.github_ref.replace('refs/tags/', '') : '');
(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 {
const _release: Release | undefined = await findTagFromReleases(releaser, owner, repo, tag);
if (_release === undefined) {
return await createRelease(
tag,
config,
releaser,
// you can't get a an existing draft by tag
// so we must find one in the list of all releases
if (config.input_draft) {
for await (const response of releaser.allReleases({
owner,
repo,
discussion_category_name,
generate_release_notes,
maxRetries,
);
repo
})) {
let release = response.data.find(release => release.tag_name === tag);
if (release) {
return release;
}
}
}
let existingRelease = await releaser.getReleaseByTag({
owner,
repo,
tag
});
let existingRelease: Release = _release!;
console.log(`Found release ${existingRelease.name} (with id=${existingRelease.id})`);
const release_id = existingRelease.id;
const release_id = existingRelease.data.id;
let target_commitish: string;
if (
config.input_target_commitish &&
config.input_target_commitish !== existingRelease.target_commitish
config.input_target_commitish !== existingRelease.data.target_commitish
) {
console.log(
`Updating commit from "${existingRelease.target_commitish}" to "${config.input_target_commitish}"`,
`Updating commit from "${existingRelease.data.target_commitish}" to "${config.input_target_commitish}"`
);
target_commitish = config.input_target_commitish;
} else {
target_commitish = existingRelease.target_commitish;
target_commitish = existingRelease.data.target_commitish;
}
const tag_name = tag;
const name = config.input_name || existingRelease.name || tag;
// revisit: support a new body-concat-strategy input for accumulating
// body parts as a release gets updated. some users will likely want this while
// others won't previously this was duplicating content for most which
// no one wants
const workflowBody = releaseBody(config) || '';
const existingReleaseBody = existingRelease.body || '';
let body: string;
if (config.input_append_body && workflowBody && existingReleaseBody) {
body = existingReleaseBody + '\n' + workflowBody;
} else {
body = workflowBody || existingReleaseBody;
}
const name = config.input_name || existingRelease.data.name || tag;
let body: string = "";
if (existingRelease.data.body) body += existingRelease.data.body;
let workflowBody = releaseBody(config);
if (existingRelease.data.body && workflowBody) body += "\n";
if (workflowBody) body += workflowBody;
const draft =
config.input_draft !== undefined
? config.input_draft
: existingRelease.data.draft;
const prerelease =
config.input_prerelease !== undefined ? config.input_prerelease : existingRelease.prerelease;
const make_latest = config.input_make_latest;
config.input_prerelease !== undefined
? config.input_prerelease
: existingRelease.data.prerelease;
const release = await releaser.updateRelease({
owner,
@@ -486,401 +253,55 @@ export const release = async (
target_commitish,
name,
body,
draft: existingRelease.draft,
draft,
prerelease,
discussion_category_name,
generate_release_notes,
make_latest,
discussion_category_name
});
return {
release: release.data,
created: false,
};
return release.data;
} catch (error) {
if (error.status !== 404) {
if (error.status === 404) {
const tag_name = tag;
const name = config.input_name || tag;
const body = releaseBody(config);
const draft = config.input_draft;
const prerelease = config.input_prerelease;
const target_commitish = config.input_target_commitish;
let commitMessage: string = "";
if (target_commitish) {
commitMessage = ` using commit "${target_commitish}"`;
}
console.log(
`⚠️ Unexpected error fetching GitHub release for tag ${config.github_ref}: ${error}`,
`👩‍🏭 Creating new GitHub release in ${owner}/${repo} for tag ${tag_name}${commitMessage}...`
);
try {
let release = await releaser.createRelease({
owner,
repo,
tag_name,
name,
body,
draft,
prerelease,
target_commitish,
discussion_category_name
});
return release.data;
} catch (error) {
// presume a race with competing metrix runs
console.log(
`⚠️ GitHub release failed with status: ${
error.status
}\n${JSON.stringify(
error.response.data.errors
)}\nretrying... (${maxRetries - 1} retries remaining)`
);
return release(config, releaser, maxRetries - 1);
}
} else {
console.log(
`⚠️ Unexpected error fetching GitHub release for tag ${config.github_ref}: ${error}`
);
throw error;
}
return await createRelease(
tag,
config,
releaser,
owner,
repo,
discussion_category_name,
generate_release_notes,
maxRetries,
);
}
};
/**
* Finalizes a release by unmarking it as "draft" (if relevant)
* after all artifacts have been uploaded.
*
* @param config - Release configuration as specified by user
* @param releaser - The GitHub API wrapper for release operations
* @param release - The existing release to be finalized
* @param maxRetries - The maximum number of attempts to finalize the release
*/
export const finalizeRelease = async (
config: Config,
releaser: Releaser,
release: Release,
releaseWasCreated: boolean = false,
maxRetries: number = 3,
): Promise<Release> => {
if (config.input_draft === true || release.draft === false) {
return release;
}
if (maxRetries <= 0) {
console.log(`❌ Too many retries. Aborting...`);
throw new Error('Too many retries.');
}
const [owner, repo] = config.github_repository.split('/');
try {
const { data } = await releaser.finalizeRelease({
owner,
repo,
release_id: release.id,
make_latest: config.input_make_latest,
});
return data;
} catch (error) {
console.warn(`error finalizing release: ${error}`);
if (releaseWasCreated && release.draft && isTagCreationBlockedError(error)) {
let deleted = false;
try {
console.log(
`🧹 Deleting draft release ${release.id} for tag ${release.tag_name} because tag creation is blocked by repository rules...`,
);
await releaser.deleteRelease({
owner,
repo,
release_id: release.id,
});
deleted = true;
} catch (cleanupError) {
console.warn(`error deleting orphan draft release ${release.id}: ${cleanupError}`);
}
const cleanupResult = deleted
? `Deleted draft release ${release.id} to avoid leaving an orphaned draft release.`
: `Failed to delete draft release ${release.id}; manual cleanup may still be required.`;
throw new Error(
`Tag creation for ${release.tag_name} is blocked by repository rules. ${cleanupResult}`,
);
}
console.log(`retrying... (${maxRetries - 1} retries remaining)`);
return finalizeRelease(config, releaser, release, releaseWasCreated, maxRetries - 1);
}
};
/**
* Lists assets belonging to a release.
*
* @param config - Release configuration as specified by user
* @param releaser - The GitHub API wrapper for release operations
* @param release - The existing release to be checked
* @param maxRetries - The maximum number of attempts
*/
export const listReleaseAssets = async (
config: Config,
releaser: Releaser,
release: Release,
maxRetries: number = 3,
): Promise<Array<{ id: number; name: string; [key: string]: any }>> => {
if (maxRetries <= 0) {
console.log(`❌ Too many retries. Aborting...`);
throw new Error('Too many retries.');
}
const [owner, repo] = config.github_repository.split('/');
try {
const assets = await releaser.listReleaseAssets({
owner,
repo,
release_id: release.id,
});
return assets;
} catch (error) {
console.warn(`error listing assets of release: ${error}`);
console.log(`retrying... (${maxRetries - 1} retries remaining)`);
return listReleaseAssets(config, releaser, release, maxRetries - 1);
}
};
/**
* Finds a release by tag name.
*
* Uses the direct getReleaseByTag API for O(1) lookup instead of iterating
* through all releases. This also avoids GitHub's API pagination limit of
* 10000 results which would cause failures for repositories with many releases.
*
* @param releaser - The GitHub API wrapper for release operations
* @param owner - The owner of the repository
* @param repo - The name of the repository
* @param tag - The tag name to search for
* @returns The release with the given tag name, or undefined if no release with that tag name is found
*/
export async function findTagFromReleases(
releaser: Releaser,
owner: string,
repo: string,
tag: string,
): Promise<Release | undefined> {
try {
const { data: release } = await releaser.getReleaseByTag({ owner, repo, tag });
return release;
} catch (error) {
// Release not found (404) or other error - return undefined to allow creation
if (error.status === 404) {
return undefined;
}
// Re-throw unexpected errors
throw error;
}
}
const CREATED_RELEASE_DISCOVERY_RETRY_DELAY_MS = 1000;
const RECENT_RELEASE_SCAN_PAGES = 2;
async function sleep(ms: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, ms));
}
async function recentReleasesByTag(
releaser: Releaser,
owner: string,
repo: string,
tag: string,
): Promise<Release[]> {
const matches: Release[] = [];
let pages = 0;
for await (const page of releaser.allReleases({ owner, repo })) {
matches.push(...page.data.filter((release) => release.tag_name === tag));
pages += 1;
if (pages >= RECENT_RELEASE_SCAN_PAGES) {
break;
}
}
return matches;
}
function pickCanonicalRelease(
releases: Release[],
releaseByTag: Release | undefined,
): Release | undefined {
if (releaseByTag && releases.some((release) => release.id === releaseByTag.id)) {
return releaseByTag;
}
if (releases.length === 0) {
return releaseByTag;
}
return [...releases].sort((left, right) => {
if (left.draft !== right.draft) {
return Number(left.draft) - Number(right.draft);
}
return left.id - right.id;
})[0];
}
async function cleanupDuplicateDraftReleases(
releaser: Releaser,
owner: string,
repo: string,
tag: string,
canonicalReleaseId: number,
recentReleases: Release[],
): Promise<void> {
for (const duplicate of recentReleases) {
if (duplicate.id === canonicalReleaseId || !duplicate.draft || duplicate.assets.length > 0) {
continue;
}
try {
console.log(`🧹 Removing duplicate draft release ${duplicate.id} for tag ${tag}...`);
await releaser.deleteRelease({
owner,
repo,
release_id: duplicate.id,
});
} catch (error) {
console.warn(`error deleting duplicate release ${duplicate.id}: ${error}`);
}
}
}
async function canonicalizeCreatedRelease(
releaser: Releaser,
owner: string,
repo: string,
tag: string,
createdRelease: Release,
maxRetries: number,
): Promise<Release> {
const attempts = Math.max(maxRetries, 1);
for (let attempt = 1; attempt <= attempts; attempt += 1) {
let releaseByTag: Release | undefined;
try {
releaseByTag = await findTagFromReleases(releaser, owner, repo, tag);
} catch (error) {
console.warn(`error reloading release for tag ${tag}: ${error}`);
}
let recentReleases: Release[] = [];
try {
recentReleases = await recentReleasesByTag(releaser, owner, repo, tag);
} catch (error) {
console.warn(`error listing recent releases for tag ${tag}: ${error}`);
}
const canonicalRelease = pickCanonicalRelease(recentReleases, releaseByTag);
if (canonicalRelease) {
if (canonicalRelease.id !== createdRelease.id) {
console.log(
`↪️ Using release ${canonicalRelease.id} for tag ${tag} instead of duplicate draft ${createdRelease.id}`,
);
}
await cleanupDuplicateDraftReleases(
releaser,
owner,
repo,
tag,
canonicalRelease.id,
recentReleases,
);
return canonicalRelease;
}
if (attempt < attempts) {
console.log(
`Release ${createdRelease.id} is not yet discoverable by tag ${tag}, retrying... (${
attempts - attempt
} retries remaining)`,
);
await sleep(CREATED_RELEASE_DISCOVERY_RETRY_DELAY_MS);
}
}
console.log(
`⚠️ Continuing with newly created release ${createdRelease.id} because tag ${tag} is still not discoverable`,
);
return createdRelease;
}
async function createRelease(
tag: string,
config: Config,
releaser: Releaser,
owner: string,
repo: string,
discussion_category_name: string | undefined,
generate_release_notes: boolean | undefined,
maxRetries: number,
): Promise<ReleaseResult> {
const tag_name = tag;
const name = config.input_name || tag;
const body = releaseBody(config);
const prerelease = config.input_prerelease;
const draft = prerelease === true ? config.input_draft === true : true;
const target_commitish = config.input_target_commitish;
const make_latest = config.input_make_latest;
let commitMessage: string = '';
if (target_commitish) {
commitMessage = ` using commit "${target_commitish}"`;
}
console.log(`👩‍🏭 Creating new GitHub release for tag ${tag_name}${commitMessage}...`);
try {
const createdRelease = await releaser.createRelease({
owner,
repo,
tag_name,
name,
body,
draft,
prerelease,
target_commitish,
discussion_category_name,
generate_release_notes,
make_latest,
});
const canonicalRelease = await canonicalizeCreatedRelease(
releaser,
owner,
repo,
tag_name,
createdRelease.data,
maxRetries,
);
return {
release: canonicalRelease,
created: canonicalRelease.id === createdRelease.data.id,
};
} catch (error) {
// presume a race with competing matrix runs
console.log(`⚠️ GitHub release failed with status: ${error.status}`);
console.log(`${JSON.stringify(error.response.data)}`);
switch (error.status) {
case 403:
console.log(
'Skip retry — your GitHub token/PAT does not have the required permission to create a release',
);
throw error;
case 404:
console.log('Skip retry - discussion category mismatch');
throw error;
case 422:
// Check if this is a race condition with "already_exists" error
const errorData = error.response?.data;
if (errorData?.errors?.[0]?.code === 'already_exists') {
console.log(
'⚠️ Release already exists (race condition detected), retrying to find and update existing release...',
);
// Don't throw - allow retry to find existing release
} else {
console.log('Skip retry - validation failed');
throw error;
}
break;
}
console.log(`retrying... (${maxRetries - 1} retries remaining)`);
return release(config, releaser, maxRetries - 1);
}
}
function isTagCreationBlockedError(error: any): boolean {
const errors = error?.response?.data?.errors;
if (!Array.isArray(errors) || error?.status !== 422) {
return false;
}
return errors.some(
({ field, message }: { field?: string; message?: string }) =>
field === 'pre_receive' &&
typeof message === 'string' &&
message.includes('creations being restricted'),
);
}
+50 -72
View File
@@ -1,25 +1,32 @@
import { setFailed, setOutput } from '@actions/core';
import { getOctokit } from '@actions/github';
import { GitHubReleaser, release, finalizeRelease, upload, listReleaseAssets } from './github';
import { isTag, parseConfig, paths, unmatchedPatterns, uploadUrl } from './util';
import {
paths,
parseConfig,
isTag,
unmatchedPatterns,
uploadUrl
} from "./util";
import { release, upload, GitHubReleaser } from "./github";
import { getOctokit } from "@actions/github";
import { setFailed, setOutput } from "@actions/core";
import { GitHub, getOctokitOptions } from "@actions/github/lib/utils";
import { env } from 'process';
import { env } from "process";
async function run() {
try {
const config = parseConfig(env);
if (!config.input_tag_name && !isTag(config.github_ref) && !config.input_draft) {
if (
!config.input_tag_name &&
!isTag(config.github_ref) &&
!config.input_draft
) {
throw new Error(`⚠️ GitHub Releases requires a tag`);
}
if (config.input_files) {
const patterns = unmatchedPatterns(config.input_files, config.input_working_directory);
patterns.forEach((pattern) => {
if (config.input_fail_on_unmatched_files) {
throw new Error(`⚠️ Pattern '${pattern}' does not match any files.`);
} else {
console.warn(`🤔 Pattern '${pattern}' does not match any files.`);
}
});
const patterns = unmatchedPatterns(config.input_files);
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`);
}
@@ -34,7 +41,9 @@ async function run() {
//new oktokit(
throttle: {
onRateLimit: (retryAfter, options) => {
console.warn(`Request quota exhausted for request ${options.method} ${options.url}`);
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!`);
@@ -43,69 +52,38 @@ async function run() {
},
onAbuseLimit: (retryAfter, options) => {
// does not retry, only logs a warning
console.warn(`Abuse detected for request ${options.method} ${options.url}`);
},
},
console.warn(
`Abuse detected for request ${options.method} ${options.url}`
);
}
}
});
//);
const releaser = new GitHubReleaser(gh);
const releaseResult = await release(config, releaser);
let rel = releaseResult.release;
const releaseWasCreated = releaseResult.created;
let uploadedAssetIds: Set<number> = new Set();
if (config.input_files && config.input_files.length > 0) {
const files = paths(config.input_files, config.input_working_directory);
const rel = await release(config, new GitHubReleaser(gh));
if (config.input_files) {
const files = paths(config.input_files);
if (files.length == 0) {
if (config.input_fail_on_unmatched_files) {
throw new Error(`⚠️ ${config.input_files} does not include a valid file.`);
} else {
console.warn(`🤔 ${config.input_files} does not include a valid file.`);
}
console.warn(`🤔 ${config.input_files} not include valid file.`);
}
const currentAssets = rel.assets;
const uploadFile = async (path: string) => {
const json = await upload(config, releaser, uploadUrl(rel.upload_url), path, currentAssets);
return json ? (json.id as number) : undefined;
};
let results: (number | undefined)[];
if (!config.input_preserve_order) {
results = await Promise.all(files.map(uploadFile));
} else {
results = [];
for (const path of files) {
results.push(await uploadFile(path));
}
}
uploadedAssetIds = new Set(results.filter((id): id is number => id !== undefined));
const currentAsserts = rel.assets;
await Promise.all(
files.map(async path => {
await upload(
config,
gh,
uploadUrl(rel.upload_url),
path,
currentAsserts
);
})
).catch(error => {
throw error;
});
}
console.log('Finalizing release...');
rel = await finalizeRelease(config, releaser, rel, releaseWasCreated);
// Draft releases use temporary "untagged-..." URLs for assets.
// URLs will be changed to correct ones once the release is published.
console.log('Getting assets list...');
{
let assets: any[] = [];
if (uploadedAssetIds.size > 0) {
const updatedAssets = await listReleaseAssets(config, releaser, rel);
assets = updatedAssets
.filter((a) => uploadedAssetIds.has(a.id))
.map((a) => {
const { uploader, ...rest } = a;
return rest;
});
}
setOutput('assets', assets);
}
console.log(`🎉 Release ready at ${rel.html_url}`);
setOutput('url', rel.html_url);
setOutput('id', rel.id.toString());
setOutput('upload_url', rel.upload_url);
setOutput("url", rel.html_url);
setOutput("id", rel.id.toString());
setOutput("upload_url", rel.upload_url);
} catch (error) {
setFailed(error.message);
}
+37 -103
View File
@@ -1,6 +1,5 @@
import * as glob from 'glob';
import { statSync, readFileSync } from 'fs';
import * as pathLib from 'path';
import * as glob from "glob";
import { lstatSync, readFileSync } from "fs";
export interface Config {
github_token: string;
@@ -13,21 +12,15 @@ export interface Config {
input_body?: string;
input_body_path?: string;
input_files?: string[];
input_working_directory?: string;
input_overwrite_files?: boolean;
input_draft?: boolean;
input_preserve_order?: 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;
input_make_latest: 'true' | 'false' | 'legacy' | undefined;
}
export const uploadUrl = (url: string): string => {
const templateMarkerPos = url.indexOf('{');
const templateMarkerPos = url.indexOf("{");
if (templateMarkerPos > -1) {
return url.substring(0, templateMarkerPos);
}
@@ -35,123 +28,64 @@ export const uploadUrl = (url: string): string => {
};
export const releaseBody = (config: Config): string | undefined => {
if (config.input_body_path) {
try {
const contents = readFileSync(config.input_body_path, 'utf8');
return contents;
} catch (err: any) {
console.warn(
`⚠️ Failed to read body_path "${config.input_body_path}" (${err?.code ?? 'ERR'}). Falling back to 'body' input.`,
);
}
}
return config.input_body;
return (
(config.input_body_path &&
readFileSync(config.input_body_path).toString("utf8")) ||
config.input_body
);
};
type Env = { [key: string]: string | undefined };
const smartSplit = (input: string): string[] => {
const result: string[] = [];
let current = '';
let braceDepth = 0;
for (const ch of input) {
if (ch === '{') {
braceDepth++;
}
if (ch === '}') {
braceDepth--;
}
if (ch === ',' && braceDepth === 0) {
if (current.trim()) {
result.push(current.trim());
}
current = '';
} else {
current += ch;
}
}
if (current.trim()) {
result.push(current.trim());
}
return result;
};
export const parseInputFiles = (files: string): string[] => {
return files
.split(/\r?\n/)
.flatMap((line) => smartSplit(line))
.filter((pat) => pat.trim() !== '');
return files.split(/\r?\n/).reduce<string[]>(
(acc, line) =>
acc
.concat(line.split(","))
.filter(pat => pat)
.map(pat => pat.trim()),
[]
);
};
export const parseConfig = (env: Env): Config => {
return {
github_token: env.GITHUB_TOKEN || env.INPUT_TOKEN || '',
github_ref: env.GITHUB_REF || '',
github_repository: env.INPUT_REPOSITORY || env.GITHUB_REPOSITORY || '',
github_token: env.GITHUB_TOKEN || env.INPUT_TOKEN || "",
github_ref: env.GITHUB_REF || "",
github_repository: env.INPUT_REPOSITORY || env.GITHUB_REPOSITORY || "",
input_name: env.INPUT_NAME,
input_tag_name: env.INPUT_TAG_NAME?.trim(),
input_body: env.INPUT_BODY,
input_body_path: env.INPUT_BODY_PATH,
input_files: parseInputFiles(env.INPUT_FILES || ''),
input_working_directory: env.INPUT_WORKING_DIRECTORY || undefined,
input_overwrite_files: env.INPUT_OVERWRITE_FILES
? env.INPUT_OVERWRITE_FILES == 'true'
input_files: parseInputFiles(env.INPUT_FILES || ""),
input_draft: env.INPUT_DRAFT ? env.INPUT_DRAFT === "true" : undefined,
input_prerelease: env.INPUT_PRERELEASE
? env.INPUT_PRERELEASE == "true"
: undefined,
input_draft: env.INPUT_DRAFT ? env.INPUT_DRAFT === 'true' : undefined,
input_preserve_order: env.INPUT_PRESERVE_ORDER ? env.INPUT_PRESERVE_ORDER == 'true' : undefined,
input_prerelease: env.INPUT_PRERELEASE ? 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',
input_make_latest: parseMakeLatest(env.INPUT_MAKE_LATEST),
input_fail_on_unmatched_files: env.INPUT_FAIL_ON_UNMATCHED_FILES == "true",
input_target_commitish: env.INPUT_TARGET_COMMITISH,
input_discussion_category_name: env.INPUT_DISCUSSION_CATEGORY_NAME
};
};
const parseMakeLatest = (value: string | undefined): 'true' | 'false' | 'legacy' | undefined => {
if (value === 'true' || value === 'false' || value === 'legacy') {
return value;
}
return undefined;
};
export const paths = (patterns: string[], cwd?: string): string[] => {
export const paths = (patterns: string[]): string[] => {
return patterns.reduce((acc: string[], pattern: string): string[] => {
const matches = glob.sync(pattern, { cwd, dot: true, absolute: false });
const resolved = matches
.map((p) => (cwd ? pathLib.join(cwd, p) : p))
.filter((p) => {
try {
return statSync(p).isFile();
} catch {
return false;
}
});
return acc.concat(resolved);
return acc.concat(
glob.sync(pattern).filter(path => lstatSync(path).isFile())
);
}, []);
};
export const unmatchedPatterns = (patterns: string[], cwd?: string): string[] => {
export const unmatchedPatterns = (patterns: string[]): string[] => {
return patterns.reduce((acc: string[], pattern: string): string[] => {
const matches = glob.sync(pattern, { cwd, dot: true, absolute: false });
const files = matches.filter((p) => {
try {
const full = cwd ? pathLib.join(cwd, p) : p;
return statSync(full).isFile();
} catch {
return false;
}
});
return acc.concat(files.length == 0 ? [pattern] : []);
return acc.concat(
glob.sync(pattern).filter(path => lstatSync(path).isFile()).length == 0
? [pattern]
: []
);
}, []);
};
export const isTag = (ref: string): boolean => {
return ref.startsWith('refs/tags/');
};
export const alignAssetName = (assetName: string): string => {
return assetName.replace(/ /g, '.');
return ref.startsWith("refs/tags/");
};
+4 -6
View File
@@ -1,10 +1,9 @@
{
"compilerOptions": {
"useUnknownInCatchVariables": false,
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es2022",
"module": "NodeNext",
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
@@ -25,7 +24,6 @@
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
"skipLibCheck": true,
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
@@ -45,7 +43,7 @@
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
"types": ["node", "vitest/globals"],
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
@@ -61,5 +59,5 @@
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"exclude": ["node_modules", "**/*.test.ts", "vitest.config.ts"]
"exclude": ["node_modules", "**/*.test.ts"]
}
-11
View File
@@ -1,11 +0,0 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
coverage: {
reporter: ['text', 'lcov'],
},
include: ['__tests__/**/*.ts'],
},
});