Compare commits

..

16 Commits

Author SHA1 Message Date
qmuntal
ad9941188f feat: split Go module and build caches
Reuse downloaded modules across Go versions, runner architectures, and
runner images on the same OS instead of duplicating them in each build
cache archive.

Restore and save each entry independently, running operations in parallel
when safe. Serialize restores for overlapping or aliased paths, preserve
the original paths for the post step, and report cache-hit only when both
entries match their primary keys.

Update the documented manual restore keys and generated action bundles.
2026-10-05 15:05:38 +02:00
Quim Muntal
90ad2b35f6 Merge pull request #794 from actions/chore/bump-actions-cache-6.3.0
chore(deps): bump @actions/cache to 6.3.0
2026-09-28 14:30:02 +02:00
qmuntal
0464ec84ca chore(deps): bump @actions/cache to 6.3.0
Pick up the Windows BSD tar temporary-file isolation fix from
actions/toolkit#2497.

Update the lockfile, license records, and generated action bundles.
2026-09-28 14:04:32 +02:00
v-mahabaleshwars
3bfff43db6 Preserve native Go environment names in action outputs (#793)
* preserve native Go environment names in action outputs

* implement review comment

* Add missing trailing newline to action.yml

---------

Co-authored-by: mahabaleshwars <147705296+mahabaleshwars@users.noreply.github.com>
2026-09-25 10:35:28 -05:00
v-mahabaleshwars
e626ada899 Expose go env variables as action outputs (#791)
* feature implementation - output go env

* update action.yml

* fix action.yml

---------

Co-authored-by: mahabaleshwars <147705296+mahabaleshwars@users.noreply.github.com>
2026-09-18 10:05:51 -05:00
dependabot[bot]
468e940882 Bump js-yaml from 3.15.1 to 3.15.2 (#790)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.15.1 to 3.15.2.
- [Changelog](https://github.com/nodeca/js-yaml/blob/3.15.2/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.15.1...3.15.2)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 3.15.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-16 12:34:25 -05:00
v-priya-kinthali
34829cb9ac fix: update macOS runner configuration to use macOS 15 Intel (#786) 2026-09-08 16:28:33 -05:00
Priya Gupta
addc0880c9 Group verbose Go installation logs and normalize package.json line endings (#789)
* refactor: enhance logging and group setup for Go version installation

* refactor: update logging group name for Go version setup

* refactor: standardize formatting in package.json

* audit fix

* update package-lock.json
2026-09-08 14:17:06 -05:00
Haritha
fba3fb4ead fix: resolve audit/license failures (#784)
* fix: resolve brace-expansion high-severity vulnerability, refresh license cache, rebuild dist

- Regenerated package-lock.json to pick up brace-expansion@5.0.9 (fixes
  GHSA-rgw5-rvv9-x895, a DoS via unbounded intermediate arrays), which
  is already permitted by minimatch's existing ^5.0.8 semver range.
- Refreshed .licenses/npm cache to match the updated dependency tree.
- Added minimatch to the licensed.yml reviewed list: its detected
  license text doesn't cleanly match Blue Oak 1.0.0, which is already
  in the allowed list.
- Rebuilt dist/setup and dist/cache-save from source.

npm audit --audit-level=high now reports 0 vulnerabilities;
licensed status reports 0 errors; npm run pre-checkin passes locally.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: narrow audit dependency updates

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-19 10:06:05 -05:00
Haritha
5064d95ba1 Upgrade checkout refs and docs to v7 (#779)
* chore: upgrade checkout/setup-go refs to v7

* chore: commit remaining dependency, license, and dist updates
2026-07-27 15:26:25 -05:00
Philip Gai
b7ad1dad31 chore(deps): bump @actions/cache to 6.2.0 (#771)
* chore(deps): bump @actions/cache to 5.2.0

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* chore(deps): update licensed record for @actions/cache 5.2.0

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* chore(deps): bump @actions/cache to 6.2.0

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 290d6c74-496d-441c-bdbe-08fc907e0d44

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-15 21:47:49 -05:00
Priya Gupta
0778a10ce4 Migrate to ESM and upgrade dependencies (#763)
* Migrate to ESM and upgrade dependencies

* Remove unused tsconfig.eslint.json

* bump version to 7.0.0 in package.json and package-lock.json

* Add ESM migration note to README for V7

* Remove unnecessary devDependencies: ts-node, @types/jest

* npm audit fix

* Update README with ESM migration details

Clarified migration details to ESM for action compatibility.

* Downgrade @types/node to ^24, clean up tsconfig and README.
2026-07-15 15:34:36 -05:00
Haritha
924ae3a1cd chore: bump version to 6.5.0 in package.json and package-lock.json (#762)
* chore: bump version to 6.5.0 in package.json and package-lock.json

* chore: update README.md for improved clarity and structure

* License update
2026-06-23 12:03:17 -05:00
Jason Ginchereau
e91cc3bfe0 Bump @actions/cache to 5.1.0, log cache write denied (#758)
* Bump @actions/cache to 5.1.0, log cache write denied

* Add cache save tests

* Re-trigger CI
2026-06-22 14:14:01 -05:00
Haritha
4a2405e6ae chore: update @types/node and @typescript-eslint dependencies to latest versions (#755) 2026-06-16 11:46:40 -05:00
Copilot
78961f6f84 chore: update @actions dependencies and refresh license cache (#744)
Agent-Logs-Url: https://github.com/actions/setup-go/sessions/0e15a7f6-9e3f-4b17-98fc-7c6501a56b31

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: priyagupta108 <147705955+priyagupta108@users.noreply.github.com>
2026-04-13 14:45:08 -05:00
73 changed files with 129122 additions and 169536 deletions

View File

@@ -1,6 +0,0 @@
# Ignore list
/*
# Do not ignore these folders:
!__tests__/
!src/

View File

@@ -1,51 +0,0 @@
// This is a reusable configuration file copied from https://github.com/actions/reusable-workflows/tree/main/reusable-configurations. Please don't make changes to this file as it's the subject of an automatic update.
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:eslint-plugin-jest/recommended',
'eslint-config-prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'eslint-plugin-node', 'eslint-plugin-jest'],
rules: {
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-ignore': 'allow-with-description'
}
],
'no-console': 'error',
'yoda': 'error',
'prefer-const': [
'error',
{
destructuring: 'all'
}
],
'no-control-regex': 'off',
'no-constant-condition': ['error', {checkLoops: false}],
'node/no-extraneous-import': 'error'
},
overrides: [
{
files: ['**/*{test,spec}.ts'],
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'jest/no-standalone-expect': 'off',
'jest/no-conditional-expect': 'off',
'no-console': 'off',
}
}
],
env: {
node: true,
es6: true,
'jest/globals': true
}
};

View File

@@ -20,7 +20,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
go-version: ['1.25', '1.24'] go-version: ['1.25', '1.24']
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Microsoft build of Go ${{ matrix.go-version }} - name: Setup Microsoft build of Go ${{ matrix.go-version }}
uses: ./ uses: ./
@@ -59,7 +59,7 @@ jobs:
env: env:
GO_DOWNLOAD_BASE_URL: 'https://aka.ms/golang/release/latest' GO_DOWNLOAD_BASE_URL: 'https://aka.ms/golang/release/latest'
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Microsoft build of Go via environment variable - name: Setup Microsoft build of Go via environment variable
uses: ./ uses: ./
@@ -95,7 +95,7 @@ jobs:
- os: macos-latest - os: macos-latest
architecture: arm64 architecture: arm64
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Microsoft build of Go with architecture - name: Setup Microsoft build of Go with architecture
uses: ./ uses: ./
@@ -116,7 +116,7 @@ jobs:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Microsoft build of Go with caching - name: Setup Microsoft build of Go with caching
uses: ./ uses: ./

View File

@@ -14,7 +14,7 @@ jobs:
steps: steps:
- name: Checking out - name: Checking out
uses: actions/checkout@v6 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Publish - name: Publish
id: publish id: publish
uses: actions/publish-immutable-action@v0.0.4 uses: actions/publish-immutable-action@v0.0.4

View File

@@ -18,9 +18,9 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest, macos-latest-large] os: [ubuntu-latest, windows-latest, macos-latest, macos-15-intel]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Go Stable - name: Setup Go Stable
uses: ./ uses: ./
with: with:
@@ -33,9 +33,9 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest, macos-latest-large] os: [ubuntu-latest, windows-latest, macos-latest, macos-15-intel]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Go oldStable - name: Setup Go oldStable
uses: ./ uses: ./
with: with:
@@ -43,21 +43,46 @@ jobs:
- name: Verify Go - name: Verify Go
run: go version run: go version
go-env-outputs:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Go
id: setup-go
uses: ./
with:
go-version: stable
- name: Show Go environment outputs
run: |
echo "GOPATH: ${{ steps.setup-go.outputs.GOPATH }}"
echo "GOBIN: ${{ steps.setup-go.outputs.GOBIN }}"
echo "go-bin-path: ${{ steps.setup-go.outputs.go-bin-path }}"
echo "GOROOT: ${{ steps.setup-go.outputs.GOROOT }}"
echo "GOCACHE: ${{ steps.setup-go.outputs.GOCACHE }}"
echo "GOMODCACHE: ${{ steps.setup-go.outputs.GOMODCACHE }}"
echo "GOOS: ${{ steps.setup-go.outputs.GOOS }}"
echo "GOARCH: ${{ steps.setup-go.outputs.GOARCH }}"
echo "GOTOOLDIR: ${{ steps.setup-go.outputs.GOTOOLDIR }}"
aliases-arch: aliases-arch:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest, macos-latest-large] os: [ubuntu-latest, windows-latest, macos-latest, macos-15-intel]
version: [stable, oldstable] version: [stable, oldstable]
architecture: [x64, x32] architecture: [x64, x32]
exclude: exclude:
- os: macos-latest - os: macos-latest
architecture: x32 architecture: x32
- os: macos-latest-large - os: macos-15-intel
architecture: x32 architecture: x32
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Go ${{ matrix.version }} ${{ matrix.architecture }} - name: Setup Go ${{ matrix.version }} ${{ matrix.architecture }}
uses: ./ uses: ./
with: with:
@@ -72,7 +97,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [macos-latest, windows-latest, ubuntu-latest, macos-latest-large] os: [macos-latest, windows-latest, ubuntu-latest, macos-15-intel]
go: [1.21.13, 1.22.8, 1.23.2] go: [1.21.13, 1.22.8, 1.23.2]
include: include:
- os: windows-latest - os: windows-latest
@@ -82,7 +107,7 @@ jobs:
go: 1.23.2 go: 1.23.2
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-go ${{ matrix.go }} - name: setup-go ${{ matrix.go }}
uses: ./ uses: ./
@@ -98,10 +123,10 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest, macos-latest-large] os: [ubuntu-latest, windows-latest, macos-latest, macos-15-intel]
go-version: ['1.20', '1.21', '1.22', '1.23'] go-version: ['1.20', '1.21', '1.22', '1.23']
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Go and check latest - name: Setup Go and check latest
uses: ./ uses: ./
with: with:
@@ -115,9 +140,9 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest, macos-latest-large] os: [ubuntu-latest, windows-latest, macos-latest, macos-15-intel]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Go and check latest - name: Setup Go and check latest
uses: ./ uses: ./
with: with:
@@ -131,9 +156,9 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest, macos-latest-large] os: [ubuntu-latest, windows-latest, macos-latest, macos-15-intel]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Go and check latest - name: Setup Go and check latest
uses: ./ uses: ./
with: with:
@@ -147,9 +172,9 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest, macos-latest-large] os: [ubuntu-latest, windows-latest, macos-latest, macos-15-intel]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Go and check latest - name: Setup Go and check latest
uses: ./ uses: ./
with: with:
@@ -163,9 +188,9 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest, macos-latest-large] os: [ubuntu-latest, windows-latest, macos-latest, macos-15-intel]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Go from .go-version file - name: Setup Go from .go-version file
uses: ./ uses: ./
with: with:
@@ -179,11 +204,11 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [macos-latest, windows-latest, ubuntu-latest, macos-latest-large] os: [macos-latest, windows-latest, ubuntu-latest, macos-15-intel]
go: [1.20.14, 1.21.10, 1.22.8, 1.23.2] go: [1.20.14, 1.21.10, 1.22.8, 1.23.2]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-go ${{ matrix.go }} - name: setup-go ${{ matrix.go }}
uses: ./ uses: ./
@@ -199,11 +224,11 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [windows-latest, ubuntu-latest, macos-latest-large] os: [windows-latest, ubuntu-latest, macos-15-intel]
go: [1.11.12] go: [1.11.12]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-go ${{ matrix.go }} - name: setup-go ${{ matrix.go }}
uses: ./ uses: ./
@@ -219,7 +244,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest, macos-latest-large] os: [ubuntu-latest, windows-latest, macos-latest, macos-15-intel]
go-version: [1.20.14, 1.21, 1.22, 1.23] go-version: [1.20.14, 1.21, 1.22, 1.23]
include: include:
- os: macos-latest - os: macos-latest
@@ -228,10 +253,10 @@ jobs:
architecture: x64 architecture: x64
- os: windows-latest - os: windows-latest
architecture: x64 architecture: x64
- os: macos-latest-large - os: macos-15-intel
architecture: x64 architecture: x64
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Go and check latest - name: Setup Go and check latest
uses: ./ uses: ./
with: with:

View File

@@ -19,7 +19,7 @@ jobs:
cache: [false, true] cache: [false, true]
go: [1.20.1] go: [1.20.1]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: 'Setup ${{ matrix.cache }}, cache: ${{ matrix.go }}' - name: 'Setup ${{ matrix.cache }}, cache: ${{ matrix.go }}'
uses: ./ uses: ./
@@ -88,7 +88,7 @@ jobs:
matrix: matrix:
cache: [false, true] cache: [false, true]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: 'Setup default go, cache: ${{ matrix.cache }}' - name: 'Setup default go, cache: ${{ matrix.cache }}'
uses: ./ uses: ./
@@ -121,7 +121,7 @@ jobs:
cache: [false] cache: [false]
go: [1.20.1] go: [1.20.1]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: 'Setup ${{ matrix.go }}, cache: ${{ matrix.cache }}' - name: 'Setup ${{ matrix.go }}, cache: ${{ matrix.cache }}'
uses: ./ uses: ./

View File

@@ -10,7 +10,10 @@ allowed:
- cc0-1.0 - cc0-1.0
- unlicense - unlicense
- 0bsd - 0bsd
- blueoak-1.0.0
reviewed: reviewed:
npm: npm:
- "@actions/http-client" - "@actions/http-client"
- "balanced-match"
- "brace-expansion"

View File

@@ -1,6 +1,6 @@
--- ---
name: "@actions/cache" name: "@actions/cache"
version: 5.0.1 version: 6.3.0
type: npm type: npm
summary: Actions cache lib summary: Actions cache lib
homepage: https://github.com/actions/toolkit/tree/main/packages/cache homepage: https://github.com/actions/toolkit/tree/main/packages/cache

View File

@@ -1,20 +0,0 @@
---
name: "@actions/core"
version: 1.11.1
type: npm
summary: Actions core lib
homepage: https://github.com/actions/toolkit/tree/main/packages/core
license: mit
licenses:
- sources: LICENSE.md
text: |-
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
notices: []

View File

@@ -1,6 +1,6 @@
--- ---
name: "@actions/core" name: "@actions/core"
version: 2.0.1 version: 3.0.1
type: npm type: npm
summary: Actions core lib summary: Actions core lib
homepage: https://github.com/actions/toolkit/tree/main/packages/core homepage: https://github.com/actions/toolkit/tree/main/packages/core

View File

@@ -1,20 +0,0 @@
---
name: "@actions/exec"
version: 2.0.0
type: npm
summary: Actions exec lib
homepage: https://github.com/actions/toolkit/tree/main/packages/exec
license: mit
licenses:
- sources: LICENSE.md
text: |-
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
notices: []

View File

@@ -1,6 +1,6 @@
--- ---
name: "@actions/exec" name: "@actions/exec"
version: 1.1.1 version: 3.0.0
type: npm type: npm
summary: Actions exec lib summary: Actions exec lib
homepage: https://github.com/actions/toolkit/tree/main/packages/exec homepage: https://github.com/actions/toolkit/tree/main/packages/exec

View File

@@ -1,6 +1,6 @@
--- ---
name: "@actions/glob" name: "@actions/glob"
version: 0.5.0 version: 0.7.0
type: npm type: npm
summary: Actions glob lib summary: Actions glob lib
homepage: https://github.com/actions/toolkit/tree/main/packages/glob homepage: https://github.com/actions/toolkit/tree/main/packages/glob

View File

@@ -1,32 +0,0 @@
---
name: "@actions/http-client"
version: 2.2.3
type: npm
summary: Actions Http Client
homepage: https://github.com/actions/toolkit/tree/main/packages/http-client
license: mit
licenses:
- sources: LICENSE
text: |
Actions Http Client for Node.js
Copyright (c) GitHub, Inc.
All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
notices: []

View File

@@ -1,6 +1,6 @@
--- ---
name: "@actions/http-client" name: "@actions/http-client"
version: 3.0.2 version: 4.0.1
type: npm type: npm
summary: Actions Http Client summary: Actions Http Client
homepage: https://github.com/actions/toolkit/tree/main/packages/http-client homepage: https://github.com/actions/toolkit/tree/main/packages/http-client

View File

@@ -1,20 +0,0 @@
---
name: "@actions/io"
version: 1.1.3
type: npm
summary: Actions io lib
homepage: https://github.com/actions/toolkit/tree/main/packages/io
license: mit
licenses:
- sources: LICENSE.md
text: |-
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
notices: []

View File

@@ -1,6 +1,6 @@
--- ---
name: "@actions/io" name: "@actions/io"
version: 2.0.0 version: 3.0.2
type: npm type: npm
summary: Actions io lib summary: Actions io lib
homepage: https://github.com/actions/toolkit/tree/main/packages/io homepage: https://github.com/actions/toolkit/tree/main/packages/io

View File

@@ -1,6 +1,6 @@
--- ---
name: "@actions/tool-cache" name: "@actions/tool-cache"
version: 2.0.2 version: 4.0.0
type: npm type: npm
summary: Actions tool-cache lib summary: Actions tool-cache lib
homepage: https://github.com/actions/toolkit/tree/main/packages/tool-cache homepage: https://github.com/actions/toolkit/tree/main/packages/tool-cache

View File

@@ -1,6 +1,6 @@
--- ---
name: "@azure/core-client" name: "@azure/core-client"
version: 1.10.1 version: 1.10.2
type: npm type: npm
summary: Core library for interfacing with AutoRest generated code summary: Core library for interfacing with AutoRest generated code
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-client/ homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-client/

View File

@@ -1,6 +1,6 @@
--- ---
name: "@azure/core-http-compat" name: "@azure/core-http-compat"
version: 2.3.1 version: 2.4.0
type: npm type: npm
summary: Core HTTP Compatibility Library to bridge the gap between Core V1 & V2 packages. summary: Core HTTP Compatibility Library to bridge the gap between Core V1 & V2 packages.
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-compat/ homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-compat/

View File

@@ -1,9 +1,9 @@
--- ---
name: "@azure/core-rest-pipeline" name: "@azure/core-rest-pipeline"
version: 1.22.2 version: 1.23.0
type: npm type: npm
summary: Isomorphic client library for making HTTP requests in node.js and browser. summary: Isomorphic client library for making HTTP requests in node.js and browser.
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-rest-pipeline/ homepage: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/core/core-rest-pipeline/README.md
license: mit license: mit
licenses: licenses:
- sources: LICENSE - sources: LICENSE

View File

@@ -1,6 +1,6 @@
--- ---
name: "@azure/core-xml" name: "@azure/core-xml"
version: 1.5.0 version: 1.5.1
type: npm type: npm
summary: Core library for interacting with XML payloads summary: Core library for interacting with XML payloads
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-xml/ homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-xml/

View File

@@ -1,9 +1,9 @@
--- ---
name: "@azure/storage-blob" name: "@azure/storage-blob"
version: 12.29.1 version: 12.31.0
type: npm type: npm
summary: Microsoft Azure Storage SDK for JavaScript - Blob summary: Microsoft Azure Storage SDK for JavaScript - Blob
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/storage/storage-blob/ homepage: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/storage/storage-blob/README.md
license: mit license: mit
licenses: licenses:
- sources: LICENSE - sources: LICENSE

View File

@@ -1,9 +1,9 @@
--- ---
name: "@azure/storage-common" name: "@azure/storage-common"
version: 12.1.1 version: 12.4.0
type: npm type: npm
summary: Azure Storage Common Client Library for JavaScript summary: Azure Storage Common Client Library for JavaScript
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/storage/storage-internal-avro/ homepage: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/storage/storage-common/README.md
license: mit license: mit
licenses: licenses:
- sources: LICENSE - sources: LICENSE

11
.licenses/npm/@nodable/entities.dep.yml generated Normal file
View File

@@ -0,0 +1,11 @@
---
name: "@nodable/entities"
version: 3.0.0
type: npm
summary: Entity parser for XML, HTML, External entites with security and NCR control
homepage:
license: mit
licenses:
- sources: README.md
text: MIT
notices: []

View File

@@ -1,9 +1,9 @@
--- ---
name: "@typespec/ts-http-runtime" name: "@typespec/ts-http-runtime"
version: 0.3.2 version: 0.3.6
type: npm type: npm
summary: Isomorphic client library for making HTTP requests in node.js and browser. summary: Isomorphic client library for making HTTP requests in node.js and browser.
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/ts-http-runtime/ homepage: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/core/ts-http-runtime/README.md
license: mit license: mit
licenses: licenses:
- sources: LICENSE - sources: LICENSE

View File

@@ -1,30 +1,35 @@
--- ---
name: "@fastify/busboy" name: anynum
version: 2.1.1 version: 1.0.1
type: npm type: npm
summary: A streaming parser for HTML form data for node.js summary: Normalize all Unicode decimal digits (Devanagari, Arabic, Thai, etc.) to
ASCII numerals. Zero dependencies, performance-first.
homepage: homepage:
license: mit license: mit
licenses: licenses:
- sources: LICENSE - sources: LICENSE
text: |- text: |
Copyright Brian White. All rights reserved. MIT License
Copyright (c) 2026 Natural Intelligence
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to of this software and associated documentation files (the "Software"), to deal
deal in the Software without restriction, including without limitation the in the Software without restriction, including without limitation the rights
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
sell copies of the Software, and to permit persons to whom the Software is copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions: furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in The above copyright notice and this permission notice shall be included in all
all copies or substantial portions of the Software. copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
IN THE SOFTWARE. SOFTWARE.
- sources: README.md
text: MIT
notices: [] notices: []

View File

@@ -1,39 +1,18 @@
--- ---
name: balanced-match name: balanced-match
version: 1.0.2 version: 4.0.4
type: npm type: npm
summary: Match balanced character pairs, like "{" and "}" summary: Match balanced character pairs, like "{" and "}"
homepage: https://github.com/juliangruber/balanced-match homepage:
license: mit license: other
licenses: licenses:
- sources: LICENSE.md - sources: LICENSE.md
text: | text: |
(MIT) (MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt; Original code Copyright Julian Gruber <julian@juliangruber.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of Port to TypeScript Copyright Isaac Z. Schlueter <i@izs.me>
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- sources: README.md
text: |-
(MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in

View File

@@ -1,16 +1,18 @@
--- ---
name: brace-expansion name: brace-expansion
version: 1.1.12 version: 5.0.9
type: npm type: npm
summary: Brace expansion as known from sh/bash summary: Brace expansion as known from sh/bash
homepage: https://github.com/juliangruber/brace-expansion homepage:
license: mit license: other
licenses: licenses:
- sources: LICENSE - sources: LICENSE
text: | text: |
MIT License MIT License
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com> Copyright Julian Gruber <julian@juliangruber.com>
TypeScript port Copyright Isaac Z. Schlueter <i@izs.me>
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -22,29 +24,6 @@ licenses:
The above copyright notice and this permission notice shall be included in all The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software. copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- sources: README.md
text: |-
(MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

View File

@@ -1,31 +0,0 @@
---
name: concat-map
version: 0.0.1
type: npm
summary: concatenative mapdashery
homepage: https://github.com/substack/node-concat-map#readme
license: other
licenses:
- sources: LICENSE
text: |
This software is released under the MIT license:
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- sources: README.markdown
text: MIT
notices: []

View File

@@ -1,6 +1,6 @@
--- ---
name: fast-xml-builder name: fast-xml-builder
version: 1.0.0 version: 1.2.0
type: npm type: npm
summary: Build XML from JSON without C/C++ based libraries summary: Build XML from JSON without C/C++ based libraries
homepage: homepage:

View File

@@ -1,6 +1,6 @@
--- ---
name: fast-xml-parser name: fast-xml-parser
version: 5.4.1 version: 5.10.1
type: npm type: npm
summary: Validate XML, Parse XML, Build XML without C/C++ based libraries summary: Validate XML, Parse XML, Build XML without C/C++ based libraries
homepage: homepage:

35
.licenses/npm/is-unsafe.dep.yml generated Normal file
View File

@@ -0,0 +1,35 @@
---
name: is-unsafe
version: 2.0.0
type: npm
summary: Zero-dependency, DOM-free, pure predicate for detecting unsafe strings across
HTML, XML, SVG, SQL, SHELL, and REGEX contexts
homepage:
license: mit
licenses:
- sources: LICENSE
text: |
MIT License
Copyright (c) 2026 Natural Intelligence
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- sources: README.md
text: MIT
notices: []

View File

@@ -1,26 +1,66 @@
--- ---
name: minimatch name: minimatch
version: 3.1.5 version: 10.2.5
type: npm type: npm
summary: a glob matcher in javascript summary: a glob matcher in javascript
homepage: homepage:
license: isc license: blueoak-1.0.0
licenses: licenses:
- sources: LICENSE - sources: LICENSE.md
text: | text: |
The ISC License # Blue Oak Model License
Copyright (c) Isaac Z. Schlueter and Contributors Version 1.0.0
Permission to use, copy, modify, and/or distribute this software for any ## Purpose
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES This license gives everyone as much permission to work with
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF this software as possible, while protecting contributors
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR from liability.
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ## Acceptance
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. In order to receive this license, you must agree to its
rules. The rules of this license are both obligations
under that agreement and conditions to your license.
You must not do anything with this software that triggers
a rule that you cannot or will not follow.
## Copyright
Each contributor licenses you to do everything with this
software that would otherwise infringe that contributor's
copyright in it.
## Notices
You must ensure that everyone who gets a copy of
any part of this software from you, with or without
changes, also gets the text of this license or a link to
<https://blueoakcouncil.org/license/1.0.0>.
## Excuse
If anyone notifies you in writing that you have not
complied with [Notices](#notices), you can keep your
license by taking all practical steps to comply within 30
days after the notice. If you do not do so, your license
ends immediately.
## Patent
Each contributor licenses you to do everything with this
software that would otherwise infringe any patent claims
they can license or become able to license.
## Reliability
No contributor can revoke this license.
## No Liability
**_As far as the law allows, this software comes as is,
without any warranty or condition, and no contributor
will be liable to anyone for any damages related to this
software or this license, under any kind of legal claim._**
notices: [] notices: []

View File

@@ -1,16 +1,16 @@
--- ---
name: "@azure/abort-controller" name: path-expression-matcher
version: 1.1.0 version: 1.6.2
type: npm type: npm
summary: Microsoft Azure SDK for JavaScript - Aborter summary: Efficient path tracking and pattern matching for XML/JSON parsers
homepage: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/core/abort-controller/README.md homepage: https://github.com/NaturalIntelligence/path-expression-matcher#readme
license: mit license: mit
licenses: licenses:
- sources: LICENSE - sources: LICENSE
text: | text: |
The MIT License (MIT) MIT License
Copyright (c) 2020 Microsoft Copyright (c) 2024
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,26 +0,0 @@
---
name: semver
version: 6.3.1
type: npm
summary: The semantic version parser used by npm.
homepage:
license: isc
licenses:
- sources: LICENSE
text: |
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
notices: []

View File

@@ -1,6 +1,6 @@
--- ---
name: semver name: semver
version: 7.7.3 version: 7.8.5
type: npm type: npm
summary: The semantic version parser used by npm. summary: The semantic version parser used by npm.
homepage: homepage:

View File

@@ -1,6 +1,6 @@
--- ---
name: strnum name: strnum
version: 2.1.2 version: 2.4.1
type: npm type: npm
summary: Parse String to Number based on configuration summary: Parse String to Number based on configuration
homepage: homepage:

View File

@@ -1,6 +1,6 @@
--- ---
name: undici name: undici
version: 6.23.0 version: 6.28.0
type: npm type: npm
summary: An HTTP/1.1 client, written from scratch for Node.js summary: An HTTP/1.1 client, written from scratch for Node.js
homepage: https://undici.nodejs.org homepage: https://undici.nodejs.org

12
.licenses/npm/xml-naming-0.1.0.dep.yml generated Normal file
View File

@@ -0,0 +1,12 @@
---
name: xml-naming
version: 0.1.0
type: npm
summary: Validates XML name productions — Name, NCName, QName, NMToken, NMTokens —
for XML 1.0 and 1.1
homepage:
license: mit
licenses:
- sources: README.md
text: MIT
notices: []

View File

@@ -1,16 +1,17 @@
--- ---
name: undici name: xml-naming
version: 5.29.0 version: 0.3.0
type: npm type: npm
summary: An HTTP/1.1 client, written from scratch for Node.js summary: Validates XML name productions — Name, NCName, QName, NMToken, NMTokens —
homepage: https://undici.nodejs.org for XML 1.0 and 1.1
homepage:
license: mit license: mit
licenses: licenses:
- sources: LICENSE - sources: LICENSE
text: | text: |
MIT License MIT License
Copyright (c) Matteo Collina and Undici contributors Copyright (c) 2026 Natural Intelligence
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,11 +0,0 @@
// This is a reusable configuration file copied from https://github.com/actions/reusable-workflows/tree/main/reusable-configurations. Please don't make changes to this file as it's the subject of an automatic update.
module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: true,
trailingComma: 'none',
bracketSpacing: false,
arrowParens: 'avoid'
};

10
.prettierrc.json Normal file
View File

@@ -0,0 +1,10 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": false,
"arrowParens": "avoid"
}

View File

@@ -9,6 +9,12 @@ This action sets up a Go environment for use in GitHub Actions by:
- Optionally caching Go modules and build outputs - Optionally caching Go modules and build outputs
- Registering problem matchers for error output - Registering problem matchers for error output
## What's new in V7
The V7 edition of the action includes:
- Migrated action internals to ESM for compatibility with latest `@actions/*` packages. No changes to action inputs, outputs, or behavior.
## Breaking changes in V6 ## Breaking changes in V6
The V6 edition of the action includes: The V6 edition of the action includes:
@@ -29,7 +35,7 @@ See [action.yml](action.yml).
<!-- start usage --> <!-- start usage -->
```yaml ```yaml
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
# Version or version range of Go to use # Version or version range of Go to use
go-version: '1.23' go-version: '1.23'
@@ -64,8 +70,8 @@ See [action.yml](action.yml).
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: '1.25' # The Go version to download (if necessary) and use. go-version: '1.25' # The Go version to download (if necessary) and use.
- run: go run hello.go - run: go run hello.go
@@ -121,6 +127,8 @@ The `cache` input is optional, and caching is enabled by default. To disable cac
By default, the action looks for `go.mod` in the repository root and uses its hash as part of the cache key. Use the `cache-dependency-path` input when you have multiple dependency files, or when they’re located in different subdirectories. This input supports glob patterns. By default, the action looks for `go.mod` in the repository root and uses its hash as part of the cache key. Use the `cache-dependency-path` input when you have multiple dependency files, or when they’re located in different subdirectories. This input supports glob patterns.
Module sources (`GOMODCACHE`) and build outputs (`GOCACHE`) are cached separately. On the same runner OS, module sources can be reused across Go versions and runner architectures, while build outputs remain partitioned by Go version, runner architecture, and Linux runner image. The `cache-hit` output is `true` only when both cache entries are restored on their primary keys.
If caching cannot be performed for any reason, the action logs a warning and continues workflow execution. If caching cannot be performed for any reason, the action logs a warning and continues workflow execution.
For examples of using `cache-dependency-path`, see the [Caching](docs/advanced-usage.md#caching) section of the [Advanced usage](docs/advanced-usage.md) guide. For examples of using `cache-dependency-path`, see the [Caching](docs/advanced-usage.md#caching) section of the [Advanced usage](docs/advanced-usage.md) guide.

View File

@@ -1,89 +1,401 @@
import * as cache from '@actions/cache'; /// <reference types="node" />
import * as core from '@actions/core';
import * as glob from '@actions/glob';
import fs from 'fs';
import * as cacheRestore from '../src/cache-restore'; import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import * as cacheUtils from '../src/cache-utils'; import fs from 'fs';
import {PackageManagerInfo} from '../src/package-managers'; import path from 'path';
jest.unstable_mockModule('@actions/cache', () => ({
saveCache: jest.fn(),
restoreCache: jest.fn(),
isFeatureAvailable: jest.fn()
}));
jest.unstable_mockModule('@actions/glob', () => ({
create: jest.fn(),
hashFiles: jest.fn()
}));
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
// Import real cache-utils (with mocked @actions) before mocking it
const realCacheUtils = await import('../src/cache-utils.js');
jest.unstable_mockModule('../src/cache-utils.js', () => ({
...realCacheUtils,
getCacheDirectoryPath: jest.fn()
}));
const cache = await import('@actions/cache');
const core = await import('@actions/core');
const glob = await import('@actions/glob');
const cacheRestore = await import('../src/cache-restore.js');
const cacheUtils = await import('../src/cache-utils.js');
const {run: saveCaches} = await import('../src/cache-save.js');
import type {CacheDirectory} from '../src/cache-utils.js';
function deferred() {
let resolve!: () => void;
const promise = new Promise<void>(done => {
resolve = done;
});
return {promise, resolve};
}
describe('restoreCache', () => { describe('restoreCache', () => {
let hashFilesSpy: jest.SpyInstance; const hashFilesSpy = glob.hashFiles as jest.Mock<typeof glob.hashFiles>;
let getCacheDirectoryPathSpy: jest.SpyInstance; const getCacheDirectoryPathSpy =
let restoreCacheSpy: jest.SpyInstance; cacheUtils.getCacheDirectoryPath as jest.Mock<
let infoSpy: jest.SpyInstance; typeof cacheUtils.getCacheDirectoryPath
let setOutputSpy: jest.SpyInstance; >;
const restoreCacheSpy = cache.restoreCache as jest.Mock<
typeof cache.restoreCache
>;
const saveCacheSpy = cache.saveCache as jest.Mock<typeof cache.saveCache>;
const infoSpy = core.info as jest.Mock<typeof core.info>;
const setOutputSpy = core.setOutput as jest.Mock<typeof core.setOutput>;
const versionSpec = '1.13.1'; const versionSpec = '1.13.1';
const packageManager = 'default'; const packageManager = 'default';
const cacheDependencyPath = 'path'; const cacheDependencyPath = 'path';
const modulePath = 'module_cache_directory';
let originalWorkspace: string | undefined; const buildPath = 'build_cache_directory';
const directories: CacheDirectory[] = [
{kind: 'modules', path: modulePath},
{kind: 'build', path: buildPath}
];
const originalEnv = {...process.env};
const originalArch = Object.getOwnPropertyDescriptor(process, 'arch')!;
const originalPlatform = Object.getOwnPropertyDescriptor(
process,
'platform'
)!;
beforeEach(() => { beforeEach(() => {
originalWorkspace = process.env.GITHUB_WORKSPACE; jest.resetAllMocks();
process.env.GITHUB_WORKSPACE = '/test/workspace'; process.env = {
//Arrange ...originalEnv,
hashFilesSpy = jest.spyOn(glob, 'hashFiles'); GITHUB_WORKSPACE: '/test/workspace',
getCacheDirectoryPathSpy = jest.spyOn(cacheUtils, 'getCacheDirectoryPath'); RUNNER_OS: 'Linux',
restoreCacheSpy = jest.spyOn(cache, 'restoreCache'); ImageOS: 'ubuntu24'
infoSpy = jest.spyOn(core, 'info'); };
setOutputSpy = jest.spyOn(core, 'setOutput'); Object.defineProperty(process, 'arch', {value: 'x64'});
Object.defineProperty(process, 'platform', {value: 'linux'});
getCacheDirectoryPathSpy.mockImplementation( hashFilesSpy.mockResolvedValue('file_hash');
(PackageManager: PackageManagerInfo) => { getCacheDirectoryPathSpy.mockResolvedValue(directories);
return Promise.resolve([ restoreCacheSpy.mockResolvedValue(undefined);
'cache_directory_path', saveCacheSpy.mockResolvedValue(1);
'cache_directory_path'
]);
}
);
}); });
afterEach(() => { afterEach(() => {
process.env.GITHUB_WORKSPACE = originalWorkspace; process.env = {...originalEnv};
Object.defineProperty(process, 'arch', originalArch);
Object.defineProperty(process, 'platform', originalPlatform);
jest.restoreAllMocks(); jest.restoreAllMocks();
}); });
const restore = (version = versionSpec) =>
cacheRestore.restoreCache(version, packageManager, cacheDependencyPath);
const requestedKeys = () => {
const moduleCall = restoreCacheSpy.mock.calls.find(
([paths]) => paths[0] === modulePath
);
const buildCall = restoreCacheSpy.mock.calls.find(
([paths]) => paths[0] === buildPath
);
expect(moduleCall).toBeDefined();
expect(buildCall).toBeDefined();
return {modules: moduleCall![1], build: buildCall![1]};
};
it('should throw if dependency file path is not valid', async () => { it('should throw if dependency file path is not valid', async () => {
// Arrange hashFilesSpy.mockResolvedValue('');
hashFilesSpy.mockImplementation(() => Promise.resolve(''));
// Act + Assert await expect(restore()).rejects.toThrow(
await expect(
cacheRestore.restoreCache(
versionSpec,
packageManager,
cacheDependencyPath
)
).rejects.toThrow(
'Some specified paths were not resolved, unable to cache dependencies.' 'Some specified paths were not resolved, unable to cache dependencies.'
); );
expect(restoreCacheSpy).not.toHaveBeenCalled();
expect(setOutputSpy).toHaveBeenLastCalledWith('cache-hit', false);
}); });
it('should inform if cache hit is not occurred', async () => { it('restores modules and build outputs as separate cache entries', async () => {
// Arrange await restore();
hashFilesSpy.mockImplementation(() => Promise.resolve('file_hash'));
restoreCacheSpy.mockImplementation(() => Promise.resolve('')); const keys = requestedKeys();
// Act + Assert expect(keys.modules).not.toBe(keys.build);
expect(restoreCacheSpy).toHaveBeenCalledTimes(2);
expect(restoreCacheSpy).toHaveBeenCalledWith([modulePath], keys.modules);
expect(restoreCacheSpy).toHaveBeenCalledWith([buildPath], keys.build);
expect(infoSpy).toHaveBeenCalledWith('modules cache is not found');
expect(infoSpy).toHaveBeenCalledWith('build cache is not found');
});
it.each([
{platform: 'linux', firstFails: false},
{platform: 'linux', firstFails: true},
{platform: 'win32', firstFails: false},
{platform: 'win32', firstFails: true}
])(
'starts both restores concurrently on $platform and waits for both (first fails=$firstFails)',
async ({platform, firstFails}) => {
Object.defineProperty(process, 'platform', {value: platform});
const started = [deferred(), deferred()];
const release = [deferred(), deferred()];
let completed = false;
restoreCacheSpy.mockImplementation(async (paths, key) => {
const index = paths[0] === modulePath ? 0 : 1;
started[index].resolve();
await release[index].promise;
if (index === 0 && firstFails) {
throw new Error('Module cache unavailable');
}
return key;
});
const pending = restore().then(() => {
completed = true;
});
try {
await Promise.all(started.map(signal => signal.promise));
release[0].resolve();
// Let the first completion propagate while the second stays blocked.
await new Promise<void>(resolve => setImmediate(resolve));
expect(completed).toBe(false);
expect(core.saveState).not.toHaveBeenCalled();
expect(setOutputSpy).not.toHaveBeenCalledWith('cache-hit', true);
} finally {
release.forEach(signal => signal.resolve());
await pending;
}
expect(setOutputSpy).toHaveBeenLastCalledWith('cache-hit', !firstFails);
expect(core.saveState).toHaveBeenCalledTimes(1);
}
);
it.each(['linux', 'win32'])(
'restores overlapping directories sequentially on %s',
async platform => {
Object.defineProperty(process, 'platform', {value: platform});
getCacheDirectoryPathSpy.mockResolvedValue([
directories[0],
{kind: 'build', path: path.join(modulePath, 'build')}
]);
const firstStarted = deferred();
const releaseFirst = deferred();
restoreCacheSpy.mockImplementation(async (paths, key) => {
if (paths[0] === modulePath) {
firstStarted.resolve();
await releaseFirst.promise;
}
return key;
});
const pending = restore();
try {
await firstStarted.promise;
expect(restoreCacheSpy).toHaveBeenCalledTimes(1);
} finally {
releaseFirst.resolve();
await pending;
}
expect(restoreCacheSpy).toHaveBeenCalledTimes(2);
expect(setOutputSpy).toHaveBeenLastCalledWith('cache-hit', true);
}
);
it.each([
{modules: false, build: false},
{modules: true, build: false},
{modules: false, build: true},
{modules: true, build: true}
])(
'reports hits and saves misses independently (modules=$modules, build=$build)',
async hits => {
const state = new Map<string, string>();
(core.saveState as jest.Mock<typeof core.saveState>).mockImplementation(
(name, value) => {
state.set(name, JSON.stringify(value));
}
);
(core.getState as jest.Mock<typeof core.getState>).mockImplementation(
name => state.get(name) || ''
);
(
core.getBooleanInput as jest.Mock<typeof core.getBooleanInput>
).mockReturnValue(true);
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
restoreCacheSpy.mockImplementation(async (paths, key) =>
hits[paths[0] === modulePath ? 'modules' : 'build'] ? key : undefined
);
await restore();
const keys = requestedKeys();
expect(setOutputSpy).toHaveBeenLastCalledWith(
'cache-hit',
hits.modules && hits.build
);
// Changing the Go environment later must not change the saved paths.
process.env.GOMODCACHE = 'different_module_cache';
process.env.GOCACHE = 'different_build_cache';
await saveCaches();
for (const {kind, path} of directories) {
if (hits[kind]) {
expect(saveCacheSpy).not.toHaveBeenCalledWith([path], keys[kind]);
} else {
expect(saveCacheSpy).toHaveBeenCalledWith([path], keys[kind]);
}
}
expect(saveCacheSpy).toHaveBeenCalledTimes(
Number(!hits.modules) + Number(!hits.build)
);
}
);
it('does not report a full hit for a non-primary cache match', async () => {
restoreCacheSpy.mockImplementation(async (_paths, key) => `${key}-other`);
await restore();
expect(setOutputSpy).toHaveBeenLastCalledWith('cache-hit', false);
});
it.each(directories)(
'continues restoring when the $kind cache fails',
async ({kind, path}) => {
restoreCacheSpy.mockImplementation(async (paths, key) => {
if (paths[0] === path) {
throw new Error('Unable to reach the service');
}
return key;
});
await restore();
expect(restoreCacheSpy).toHaveBeenCalledTimes(2);
expect(core.warning).toHaveBeenCalledWith(
`Restore ${kind} cache failed: Unable to reach the service`
);
expect(setOutputSpy).toHaveBeenLastCalledWith('cache-hit', false);
}
);
it.each(directories)(
'restores only $kind when the other directory cannot be resolved',
async directory => {
getCacheDirectoryPathSpy.mockResolvedValue([directory]);
restoreCacheSpy.mockImplementation(async (_paths, key) => key);
await restore();
expect(restoreCacheSpy).toHaveBeenCalledTimes(1);
expect(restoreCacheSpy).toHaveBeenCalledWith(
[directory.path],
expect.any(String)
);
expect(setOutputSpy).toHaveBeenLastCalledWith('cache-hit', false);
}
);
it('reuses modules across Go versions without reusing build outputs', async () => {
await restore();
const before = requestedKeys();
restoreCacheSpy.mockClear();
await restore('1.26.1');
const after = requestedKeys();
expect(after.modules).toBe(before.modules);
expect(after.build).not.toBe(before.build);
});
it.each([
{
name: 'runner architecture',
change: () => Object.defineProperty(process, 'arch', {value: 'arm64'})
},
{
name: 'runner image',
change: () => {
process.env.ImageOS = 'ubuntu26';
}
}
])('reuses only modules when the $name changes', async ({change}) => {
await restore();
const before = requestedKeys();
restoreCacheSpy.mockClear();
change();
await restore();
const after = requestedKeys();
expect(after.modules).toBe(before.modules);
expect(after.build).not.toBe(before.build);
});
it('keeps both cache entries isolated by runner OS', async () => {
await restore();
const before = requestedKeys();
restoreCacheSpy.mockClear();
process.env.RUNNER_OS = 'Windows';
await restore();
const after = requestedKeys();
expect(after.modules).not.toBe(before.modules);
expect(after.build).not.toBe(before.build);
});
it('invalidates both cache entries when the dependency hash changes', async () => {
await restore();
const before = requestedKeys();
restoreCacheSpy.mockClear();
hashFilesSpy.mockResolvedValue('different_file_hash');
await restore();
const after = requestedKeys();
expect(after.modules).not.toBe(before.modules);
expect(after.build).not.toBe(before.build);
});
it('hashes the configured dependency paths for both cache entries', async () => {
const dependencyPaths = 'go.sum\ntools/go.sum';
await cacheRestore.restoreCache( await cacheRestore.restoreCache(
versionSpec, versionSpec,
packageManager, packageManager,
cacheDependencyPath dependencyPaths
); );
expect(infoSpy).toHaveBeenCalledWith('Cache is not found');
});
it('should set output if cache hit is occurred', async () => { expect(hashFilesSpy).toHaveBeenCalledWith(dependencyPaths);
// Arrange expect(restoreCacheSpy).toHaveBeenCalledTimes(2);
hashFilesSpy.mockImplementation(() => Promise.resolve('file_hash'));
restoreCacheSpy.mockImplementation(() => Promise.resolve('cache_key'));
// Act + Assert
await cacheRestore.restoreCache(
versionSpec,
packageManager,
cacheDependencyPath
);
expect(setOutputSpy).toHaveBeenCalledWith('cache-hit', true);
}); });
it('should throw if dependency file is not found in workspace', async () => { it('should throw if dependency file is not found in workspace', async () => {

View File

@@ -0,0 +1,306 @@
/// <reference types="node" />
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
jest.unstable_mockModule('@actions/cache', () => ({
saveCache: jest.fn(),
restoreCache: jest.fn(),
isFeatureAvailable: jest.fn()
}));
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
setFailed: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getState: jest.fn(),
saveState: jest.fn()
}));
const realFs = (await import('fs')).default;
const fsExports = {...realFs, existsSync: jest.fn()};
jest.unstable_mockModule('fs', () => ({...fsExports, default: fsExports}));
const cache = await import('@actions/cache');
const core = await import('@actions/core');
const fs = (await import('fs')).default;
const {run} = await import('../src/cache-save.js');
import type {CacheEntry} from '../src/cache-utils.js';
function deferred() {
let resolve!: () => void;
const promise = new Promise<void>(done => {
resolve = done;
});
return {promise, resolve};
}
describe('cache-save', () => {
let entries: CacheEntry[];
let getBooleanInputSpy: jest.Mock<typeof core.getBooleanInput>;
let getStateSpy: jest.Mock<typeof core.getState>;
let infoSpy: jest.Mock<typeof core.info>;
let warningSpy: jest.Mock<typeof core.warning>;
let debugSpy: jest.Mock<typeof core.debug>;
let setFailedSpy: jest.Mock<typeof core.setFailed>;
let saveCacheSpy: jest.Mock<typeof cache.saveCache>;
let existsSpy: jest.Mock<typeof fs.existsSync>;
beforeEach(() => {
entries = [
{
kind: 'modules',
path: 'module_cache_directory',
primaryKey: 'modules-primary-key'
},
{
kind: 'build',
path: 'build_cache_directory',
primaryKey: 'build-primary-key'
}
];
getBooleanInputSpy = core.getBooleanInput as jest.Mock<
typeof core.getBooleanInput
>;
getBooleanInputSpy.mockReturnValue(true);
getStateSpy = core.getState as jest.Mock<typeof core.getState>;
getStateSpy.mockImplementation(() => JSON.stringify(entries));
infoSpy = core.info as jest.Mock<typeof core.info>;
infoSpy.mockImplementation(() => undefined);
warningSpy = core.warning as jest.Mock<typeof core.warning>;
warningSpy.mockImplementation(() => undefined);
debugSpy = core.debug as jest.Mock<typeof core.debug>;
debugSpy.mockImplementation(() => undefined);
setFailedSpy = core.setFailed as jest.Mock<typeof core.setFailed>;
setFailedSpy.mockImplementation(() => undefined);
saveCacheSpy = cache.saveCache as jest.Mock<typeof cache.saveCache>;
saveCacheSpy.mockImplementation(() => Promise.resolve(0));
existsSpy = fs.existsSync as jest.Mock<typeof fs.existsSync>;
existsSpy.mockImplementation(() => true);
});
afterEach(() => {
jest.restoreAllMocks();
jest.clearAllMocks();
});
it('does not save cache when the cache input is false', async () => {
getBooleanInputSpy.mockReturnValue(false);
await run();
expect(saveCacheSpy).not.toHaveBeenCalled();
expect(warningSpy).not.toHaveBeenCalled();
expect(setFailedSpy).not.toHaveBeenCalled();
});
it.each([false, true])(
'starts both saves concurrently and waits before exiting (first fails=%s)',
async firstFails => {
const started = [deferred(), deferred()];
const release = [deferred(), deferred()];
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
return undefined as never;
});
saveCacheSpy.mockImplementation(async paths => {
const index = paths[0] === entries[0].path ? 0 : 1;
started[index].resolve();
await release[index].promise;
if (index === 0 && firstFails) {
throw new Error('Module cache unavailable');
}
return 1;
});
const pending = run(true);
try {
await Promise.all(started.map(signal => signal.promise));
release[0].resolve();
// Let the first completion propagate while the second stays blocked.
await new Promise<void>(resolve => setImmediate(resolve));
expect(exitSpy).not.toHaveBeenCalled();
} finally {
release.forEach(signal => signal.resolve());
await pending;
}
expect(exitSpy).toHaveBeenCalledTimes(1);
expect(exitSpy).toHaveBeenCalledWith(0);
expect(infoSpy).toHaveBeenCalledWith(
`Cache saved with the key: ${entries[1].primaryKey}`
);
expect(setFailedSpy).not.toHaveBeenCalled();
}
);
it('does not save cache when there are no cache folders on the disk', async () => {
existsSpy.mockImplementation(() => false);
await run();
for (const {kind, path} of entries) {
expect(infoSpy).toHaveBeenCalledWith(
`[warning]${kind} cache folder doesn't exist on disk: ${path}`
);
}
expect(saveCacheSpy).not.toHaveBeenCalled();
expect(setFailedSpy).not.toHaveBeenCalled();
});
it.each(['modules', 'build'])(
'saves the other cache when the %s directory is missing',
async kind => {
const missing = entries.find(entry => entry.kind === kind)!;
const existing = entries.find(entry => entry.kind !== kind)!;
existsSpy.mockImplementation(path => path !== missing.path);
await run();
expect(saveCacheSpy).toHaveBeenCalledTimes(1);
expect(saveCacheSpy).toHaveBeenCalledWith(
[existing.path],
existing.primaryKey
);
expect(setFailedSpy).not.toHaveBeenCalled();
}
);
it('does not save cache when no cache entries were prepared', async () => {
getStateSpy.mockReturnValue('');
await run();
expect(infoSpy).toHaveBeenCalledWith(
'Cache keys were not generated. Please check the log messages above for more errors or information'
);
expect(saveCacheSpy).not.toHaveBeenCalled();
expect(setFailedSpy).not.toHaveBeenCalled();
});
it('does not fail the workflow if the saved state is invalid', async () => {
getStateSpy.mockReturnValue('{');
await run();
expect(warningSpy).toHaveBeenCalledWith(expect.any(String));
expect(saveCacheSpy).not.toHaveBeenCalled();
expect(setFailedSpy).not.toHaveBeenCalled();
});
it('does not save either cache when both primary keys were hit', async () => {
for (const entry of entries) {
entry.matchedKey = entry.primaryKey;
}
await run();
expect(saveCacheSpy).not.toHaveBeenCalled();
expect(warningSpy).not.toHaveBeenCalled();
expect(setFailedSpy).not.toHaveBeenCalled();
});
it.each(['modules', 'build'])(
'skips only the %s cache when its primary key was hit',
async kind => {
const hit = entries.find(entry => entry.kind === kind)!;
const miss = entries.find(entry => entry.kind !== kind)!;
hit.matchedKey = hit.primaryKey;
await run();
expect(saveCacheSpy).toHaveBeenCalledTimes(1);
expect(saveCacheSpy).toHaveBeenCalledWith([miss.path], miss.primaryKey);
expect(warningSpy).not.toHaveBeenCalled();
expect(setFailedSpy).not.toHaveBeenCalled();
}
);
it('saves both cache directories under separate keys on a cold run', async () => {
await run();
expect(saveCacheSpy).toHaveBeenCalledTimes(2);
for (const {path, primaryKey} of entries) {
expect(saveCacheSpy).toHaveBeenCalledWith([path], primaryKey);
expect(infoSpy).toHaveBeenCalledWith(
`Cache saved with the key: ${primaryKey}`
);
}
expect(warningSpy).not.toHaveBeenCalled();
expect(setFailedSpy).not.toHaveBeenCalled();
});
it('saves caches restored from non-primary matches', async () => {
for (const entry of entries) {
entry.matchedKey = `${entry.primaryKey}-other`;
}
await run();
expect(saveCacheSpy).toHaveBeenCalledTimes(2);
expect(warningSpy).not.toHaveBeenCalled();
expect(setFailedSpy).not.toHaveBeenCalled();
});
it.each(['modules', 'build'])(
'continues saving when the %s cache reservation fails',
async kind => {
const failed = entries.find(entry => entry.kind === kind)!;
const saved = entries.find(entry => entry.kind !== kind)!;
saveCacheSpy.mockImplementation(async paths =>
paths[0] === failed.path ? -1 : 1
);
await run();
expect(saveCacheSpy).toHaveBeenCalledTimes(2);
expect(debugSpy).toHaveBeenCalledWith(
`Cache was not saved for the key: ${failed.primaryKey}`
);
expect(infoSpy).not.toHaveBeenCalledWith(
`Cache saved with the key: ${failed.primaryKey}`
);
expect(infoSpy).toHaveBeenCalledWith(
`Cache saved with the key: ${saved.primaryKey}`
);
expect(warningSpy).not.toHaveBeenCalled();
expect(setFailedSpy).not.toHaveBeenCalled();
}
);
it.each(['modules', 'build'])(
'continues saving when the %s cache upload throws',
async kind => {
const failed = entries.find(entry => entry.kind === kind)!;
const saved = entries.find(entry => entry.kind !== kind)!;
saveCacheSpy.mockImplementation(async paths => {
if (paths[0] === failed.path) {
throw new Error('Unable to reach the service');
}
return 1;
});
await run();
expect(saveCacheSpy).toHaveBeenCalledTimes(2);
expect(warningSpy).toHaveBeenCalledWith(
`Save ${kind} cache failed: Unable to reach the service`
);
expect(infoSpy).toHaveBeenCalledWith(
`Cache saved with the key: ${saved.primaryKey}`
);
expect(setFailedSpy).not.toHaveBeenCalled();
}
);
});

View File

@@ -1,22 +1,70 @@
import * as exec from '@actions/exec'; import {
import * as cache from '@actions/cache'; jest,
import * as core from '@actions/core'; describe,
import * as cacheUtils from '../src/cache-utils'; it,
import {PackageManagerInfo} from '../src/package-managers'; expect,
beforeEach,
afterEach,
afterAll
} from '@jest/globals';
import fs from 'fs';
import path from 'path';
jest.unstable_mockModule('@actions/exec', () => ({
exec: jest.fn(),
getExecOutput: jest.fn()
}));
jest.unstable_mockModule('@actions/cache', () => ({
saveCache: jest.fn(),
restoreCache: jest.fn(),
isFeatureAvailable: jest.fn()
}));
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
const exec = await import('@actions/exec');
const cache = await import('@actions/cache');
const core = await import('@actions/core');
const cacheUtils = await import('../src/cache-utils.js');
import type {PackageManagerInfo} from '../src/package-managers.js';
describe('getCommandOutput', () => { describe('getCommandOutput', () => {
//Arrange //Arrange
const getExecOutputSpy = jest.spyOn(exec, 'getExecOutput'); const getExecOutputSpy = exec.getExecOutput as jest.Mock<
typeof exec.getExecOutput
>;
it('should return trimmed stdout in case of successful exit code', async () => { it('should return trimmed stdout in case of successful exit code', async () => {
//Arrange //Arrange
const stdoutResult = ' stdout '; const stdoutResult = ' stdout ';
const trimmedStdout = stdoutResult.trim(); const trimmedStdout = stdoutResult.trim();
getExecOutputSpy.mockImplementation((commandLine: string) => { getExecOutputSpy.mockImplementation(async (commandLine: string) => {
return new Promise<exec.ExecOutput>(resolve => { return {exitCode: 0, stdout: stdoutResult, stderr: ''};
resolve({exitCode: 0, stdout: stdoutResult, stderr: ''});
});
}); });
//Act + Assert //Act + Assert
@@ -29,10 +77,8 @@ describe('getCommandOutput', () => {
//Arrange //Arrange
const stderrResult = 'error message'; const stderrResult = 'error message';
getExecOutputSpy.mockImplementation((commandLine: string) => { getExecOutputSpy.mockImplementation(async (commandLine: string) => {
return new Promise<exec.ExecOutput>(resolve => { return {exitCode: 10, stdout: '', stderr: stderrResult};
resolve({exitCode: 10, stdout: '', stderr: stderrResult});
});
}); });
//Act + Assert //Act + Assert
@@ -48,7 +94,10 @@ describe('getPackageManagerInfo', () => {
const packageManagerName = 'default'; const packageManagerName = 'default';
const expectedResult = { const expectedResult = {
dependencyFilePattern: 'go.mod', dependencyFilePattern: 'go.mod',
cacheFolderCommandList: ['go env GOMODCACHE', 'go env GOCACHE'] cacheFolderCommandList: [
{kind: 'modules', command: 'go env GOMODCACHE'},
{kind: 'build', command: 'go env GOCACHE'}
]
}; };
//Act + Assert //Act + Assert
@@ -70,22 +119,35 @@ describe('getPackageManagerInfo', () => {
describe('getCacheDirectoryPath', () => { describe('getCacheDirectoryPath', () => {
//Arrange //Arrange
const getExecOutputSpy = jest.spyOn(exec, 'getExecOutput'); const getExecOutputSpy = exec.getExecOutput as jest.Mock<
typeof exec.getExecOutput
>;
const validPackageManager: PackageManagerInfo = { const validPackageManager: PackageManagerInfo = {
dependencyFilePattern: 'go.mod', dependencyFilePattern: 'go.mod',
cacheFolderCommandList: ['go env GOMODCACHE', 'go env GOCACHE'] cacheFolderCommandList: [
{kind: 'modules', command: 'go env GOMODCACHE'},
{kind: 'build', command: 'go env GOCACHE'}
]
}; };
it('should return path to the cache folders which specified package manager uses', async () => { it('should return path to the cache folders which specified package manager uses', async () => {
//Arrange //Arrange
getExecOutputSpy.mockImplementation((commandLine: string) => { getExecOutputSpy.mockImplementation(async (commandLine: string) => {
return new Promise<exec.ExecOutput>(resolve => { return {
resolve({exitCode: 0, stdout: 'path/to/cache/folder', stderr: ''}); exitCode: 0,
}); stdout:
commandLine === 'go env GOMODCACHE'
? 'path/to/modules'
: 'path/to/build',
stderr: ''
};
}); });
const expectedResult = ['path/to/cache/folder', 'path/to/cache/folder']; const expectedResult = [
{kind: 'modules', path: 'path/to/modules'},
{kind: 'build', path: 'path/to/build'}
];
//Act + Assert //Act + Assert
return cacheUtils return cacheUtils
@@ -93,33 +155,43 @@ describe('getCacheDirectoryPath', () => {
.then(data => expect(data).toEqual(expectedResult)); .then(data => expect(data).toEqual(expectedResult));
}); });
it('should return path to the cache folder if one command return empty str', async () => { it.each(validPackageManager.cacheFolderCommandList)(
//Arrange 'preserves the $kind identity when the other command returns an empty path',
getExecOutputSpy.mockImplementationOnce((commandLine: string) => { async ({kind, command}) => {
return new Promise<exec.ExecOutput>(resolve => { getExecOutputSpy.mockImplementation(async commandLine => ({
resolve({exitCode: 0, stdout: 'path/to/cache/folder', stderr: ''}); exitCode: 0,
}); stdout: commandLine === command ? 'path/to/cache/folder' : '',
stderr: ''
}));
await expect(
cacheUtils.getCacheDirectoryPath(validPackageManager)
).resolves.toEqual([{kind, path: 'path/to/cache/folder'}]);
}
);
it.each(validPackageManager.cacheFolderCommandList)(
'preserves the $kind identity when the other command fails',
async ({kind, command}) => {
getExecOutputSpy.mockImplementation(async commandLine => {
if (commandLine !== command) {
throw new Error('Could not run go env');
}
return {exitCode: 0, stdout: 'path/to/cache/folder', stderr: ''};
}); });
getExecOutputSpy.mockImplementationOnce((commandLine: string) => { await expect(
return new Promise<exec.ExecOutput>(resolve => { cacheUtils.getCacheDirectoryPath(validPackageManager)
resolve({exitCode: 0, stdout: '', stderr: ''}); ).resolves.toEqual([{kind, path: 'path/to/cache/folder'}]);
}); expect(core.info).toHaveBeenCalledWith(
}); expect.stringContaining('Could not run go env')
);
const expectedResult = ['path/to/cache/folder']; }
);
//Act + Assert
return cacheUtils
.getCacheDirectoryPath(validPackageManager)
.then(data => expect(data).toEqual(expectedResult));
});
it('should throw if the both commands return empty str', async () => { it('should throw if the both commands return empty str', async () => {
getExecOutputSpy.mockImplementation((commandLine: string) => { getExecOutputSpy.mockImplementation(async (commandLine: string) => {
return new Promise<exec.ExecOutput>(resolve => { return {exitCode: 0, stdout: '', stderr: ''};
resolve({exitCode: 10, stdout: '', stderr: ''});
});
}); });
//Act + Assert //Act + Assert
@@ -128,11 +200,9 @@ describe('getCacheDirectoryPath', () => {
}).rejects.toThrow(); }).rejects.toThrow();
}); });
it('should throw if the specified package name is invalid', async () => { it('should throw if both commands fail', async () => {
getExecOutputSpy.mockImplementation((commandLine: string) => { getExecOutputSpy.mockImplementation(async (commandLine: string) => {
return new Promise<exec.ExecOutput>(resolve => { return {exitCode: 10, stdout: '', stderr: 'Error message'};
resolve({exitCode: 10, stdout: '', stderr: 'Error message'});
});
}); });
//Act + Assert //Act + Assert
@@ -142,10 +212,145 @@ describe('getCacheDirectoryPath', () => {
}); });
}); });
describe('canRestoreCachesInParallel', () => {
const originalPlatform = Object.getOwnPropertyDescriptor(
process,
'platform'
)!;
const directoryStat = fs.lstatSync(process.cwd());
beforeEach(() => {
Object.defineProperty(process, 'platform', {value: 'linux'});
jest.spyOn(fs, 'lstatSync').mockReturnValue(directoryStat);
jest
.spyOn(fs, 'realpathSync')
.mockImplementation(directory => path.resolve(directory.toString()));
});
afterEach(() => {
Object.defineProperty(process, 'platform', originalPlatform);
jest.restoreAllMocks();
});
it.each([
{modules: 'cache/modules', build: 'cache/build', parallel: true},
{modules: 'cache', build: 'cache-build', parallel: true},
{modules: 'cache', build: 'cache', parallel: false},
{modules: 'cache', build: 'cache/build', parallel: false},
{modules: 'cache/modules', build: 'cache', parallel: false},
{modules: 'cache', build: 'cache/../cache', parallel: false}
])(
'checks overlapping paths: $modules and $build',
({modules, build, parallel}) => {
expect(
cacheUtils.canRestoreCachesInParallel([
{kind: 'modules', path: modules},
{kind: 'build', path: build}
])
).toBe(parallel);
}
);
it('allows concurrent Windows restores with disjoint paths', () => {
Object.defineProperty(process, 'platform', {value: 'win32'});
expect(
cacheUtils.canRestoreCachesInParallel([
{kind: 'modules', path: 'cache/modules'},
{kind: 'build', path: 'cache/build'}
])
).toBe(true);
});
it.each(['darwin', 'win32'])(
'handles case-insensitive paths conservatively on %s',
platform => {
Object.defineProperty(process, 'platform', {value: platform});
expect(
cacheUtils.canRestoreCachesInParallel([
{kind: 'modules', path: 'cache'},
{kind: 'build', path: 'CACHE/build'}
])
).toBe(false);
}
);
it.each([true, false])(
'serializes symlink aliases with missing cache directories (overlap=%s)',
overlap => {
const alias = path.resolve('alias');
const target = path.resolve('target');
jest.spyOn(fs, 'lstatSync').mockImplementation(directory => {
if (directory !== alias && directory !== target) {
throw Object.assign(new Error('Missing path'), {code: 'ENOENT'});
}
return directoryStat;
});
jest.spyOn(fs, 'realpathSync').mockReturnValue(target);
expect(
cacheUtils.canRestoreCachesInParallel([
{kind: 'modules', path: path.join(alias, 'modules')},
{
kind: 'build',
path: path.join(target, overlap ? 'modules/build' : 'build')
}
])
).toBe(false);
}
);
it.each(['nested alias', 'alias through module cache'])(
'serializes restores when the %s resolves outside the module cache',
scenario => {
const modules = path.resolve('modules');
const target = path.resolve('external-build');
const alias = path.join(modules, 'build-link');
const build =
scenario === 'nested alias'
? path.join(alias, 'cache')
: path.resolve('build-alias', 'cache');
jest.spyOn(fs, 'realpathSync').mockImplementation(directory => {
const absolute = path.resolve(directory.toString());
if (absolute === build) {
return path.join(target, 'cache');
}
if (absolute === path.dirname(build)) {
return scenario === 'nested alias' ? target : alias;
}
return absolute;
});
expect(
cacheUtils.canRestoreCachesInParallel([
{kind: 'modules', path: modules},
{kind: 'build', path: build}
])
).toBe(false);
}
);
it('falls back to sequential restores when path resolution fails', () => {
jest.spyOn(fs, 'realpathSync').mockImplementation(() => {
throw new Error('Cannot resolve path');
});
expect(
cacheUtils.canRestoreCachesInParallel([
{kind: 'modules', path: 'cache/modules'},
{kind: 'build', path: 'cache/build'}
])
).toBe(false);
});
});
describe('isCacheFeatureAvailable', () => { describe('isCacheFeatureAvailable', () => {
//Arrange //Arrange
const isFeatureAvailableSpy = jest.spyOn(cache, 'isFeatureAvailable'); const isFeatureAvailableSpy = cache.isFeatureAvailable as jest.Mock<
const warningSpy = jest.spyOn(core, 'warning'); typeof cache.isFeatureAvailable
>;
const warningSpy = core.warning as jest.Mock<typeof core.warning>;
it('should return true when cache feature is available', () => { it('should return true when cache feature is available', () => {
//Arrange //Arrange
@@ -211,10 +416,9 @@ describe('isCacheFeatureAvailable', () => {
}); });
describe('isGhes', () => { describe('isGhes', () => {
const pristineEnv = process.env; const pristineEnv = {...process.env};
beforeEach(() => { beforeEach(() => {
jest.resetModules();
process.env = {...pristineEnv}; process.env = {...pristineEnv};
}); });

View File

@@ -1,17 +1,101 @@
import * as core from '@actions/core'; import {
import * as io from '@actions/io'; jest,
import * as tc from '@actions/tool-cache'; describe,
it,
expect,
beforeAll,
beforeEach,
afterEach,
afterAll
} from '@jest/globals';
import fs from 'fs'; import fs from 'fs';
import cp from 'child_process'; import cp from 'child_process';
import osm, {type} from 'os';
import path from 'path';
import * as main from '../src/main';
import * as im from '../src/installer';
import * as httpm from '@actions/http-client';
import goJsonData from './data/golang-dl.json'; import goJsonData from './data/golang-dl.json' with {type: 'json'};
import matchers from '../matchers.json'; import matchers from '../matchers.json' with {type: 'json'};
import goTestManifest from './data/versions-manifest.json'; import goTestManifest from './data/versions-manifest.json' with {type: 'json'};
import type {IGoVersion} from '../src/installer.js';
import type {IToolRelease} from '@actions/tool-cache';
import {GO_ENV_OUTPUTS} from '../src/constants.js';
const httpClientGetJson = jest.fn();
const realCore = await import('@actions/core');
jest.unstable_mockModule('@actions/core', () => ({
...realCore,
getInput: jest.fn(),
getBooleanInput: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
warning: jest.fn(),
setOutput: jest.fn(),
exportVariable: jest.fn()
}));
const realOs = (await import('os')).default;
const osPlatformMock = jest.fn();
const osArchMock = jest.fn();
const osExports = {
...realOs,
platform: osPlatformMock,
arch: osArchMock
};
jest.unstable_mockModule('os', () => ({...osExports, default: osExports}));
const realPath = (await import('path')).default;
const pathJoinMock = jest.fn();
const pathExports = {
...realPath,
join: pathJoinMock
};
jest.unstable_mockModule('path', () => ({
...pathExports,
default: pathExports
}));
jest.unstable_mockModule('@actions/io', () => ({
which: jest.fn(),
mkdirP: jest.fn(),
rmRF: jest.fn(),
mv: jest.fn(),
cp: jest.fn()
}));
const realTc = await import('@actions/tool-cache');
jest.unstable_mockModule('@actions/tool-cache', () => ({
...realTc,
find: jest.fn(),
downloadTool: jest.fn(),
extractTar: jest.fn(),
extractZip: jest.fn(),
cacheDir: jest.fn(),
getManifestFromRepo: jest.fn()
}));
const realHttp = await import('@actions/http-client');
jest.unstable_mockModule('@actions/http-client', () => ({
...realHttp,
HttpClient: jest.fn().mockImplementation(() => ({
getJson: httpClientGetJson
}))
}));
jest.unstable_mockModule('../src/go-version-fetch.js', () => ({
getVersionsDist: jest.fn()
}));
const core = await import('@actions/core');
const io = await import('@actions/io');
const tc = await import('@actions/tool-cache');
const vf = await import('../src/go-version-fetch.js');
const main = await import('../src/main.js');
const im = await import('../src/installer.js');
const osm = (await import('os')).default;
const path = (await import('path')).default;
const matcherPattern = matchers.problemMatcher[0].pattern[0]; const matcherPattern = matchers.problemMatcher[0].pattern[0];
const matcherRegExp = new RegExp(matcherPattern.regexp); const matcherRegExp = new RegExp(matcherPattern.regexp);
const win32Join = path.win32.join; const win32Join = path.win32.join;
@@ -23,32 +107,33 @@ describe('setup-go', () => {
let inputs = {} as any; let inputs = {} as any;
let os = {} as any; let os = {} as any;
let inSpy: jest.SpyInstance; let inSpy: jest.Mock<typeof core.getInput>;
let getBooleanInputSpy: jest.SpyInstance; let getBooleanInputSpy: jest.Mock<typeof core.getBooleanInput>;
let exportVarSpy: jest.SpyInstance; let exportVarSpy: jest.Mock<typeof core.exportVariable>;
let findSpy: jest.SpyInstance; let findSpy: jest.Mock;
let cnSpy: jest.SpyInstance; let cnSpy: jest.SpiedFunction<typeof process.stdout.write>;
let logSpy: jest.SpyInstance; let logSpy: jest.Mock;
let getSpy: jest.SpyInstance; let setOutputSpy: jest.Mock<typeof core.setOutput>;
let platSpy: jest.SpyInstance; let warningSpy: jest.Mock<typeof core.warning>;
let archSpy: jest.SpyInstance; let getSpy: jest.Mock;
let joinSpy: jest.SpyInstance; let platSpy: jest.Mock;
let dlSpy: jest.SpyInstance; let archSpy: jest.Mock;
let extractTarSpy: jest.SpyInstance; let joinSpy: jest.Mock<typeof path.join>;
let extractZipSpy: jest.SpyInstance; let dlSpy: jest.Mock;
let cacheSpy: jest.SpyInstance; let extractTarSpy: jest.Mock;
let dbgSpy: jest.SpyInstance; let extractZipSpy: jest.Mock;
let whichSpy: jest.SpyInstance; let cacheSpy: jest.Mock;
let existsSpy: jest.SpyInstance; let dbgSpy: jest.Mock;
let readFileSpy: jest.SpyInstance; let whichSpy: jest.Mock;
let mkdirpSpy: jest.SpyInstance; let existsSpy: jest.SpiedFunction<typeof fs.existsSync>;
let mkdirSpy: jest.SpyInstance; let readFileSpy: jest.SpiedFunction<typeof fs.readFileSync>;
let symlinkSpy: jest.SpyInstance; let mkdirpSpy: jest.Mock;
let execSpy: jest.SpyInstance; let mkdirSpy: jest.SpiedFunction<typeof fs.mkdir>;
let execFileSpy: jest.SpyInstance; let symlinkSpy: jest.SpiedFunction<typeof fs.symlinkSync>;
let getManifestSpy: jest.SpyInstance; let execSpy: jest.SpiedFunction<typeof cp.execSync>;
let getAllVersionsSpy: jest.SpyInstance; let execFileSpy: jest.SpiedFunction<typeof cp.execFileSync>;
let httpmGetJsonSpy: jest.SpyInstance; let getManifestSpy: jest.Mock;
let httpmGetJsonSpy: jest.Mock;
beforeAll(async () => { beforeAll(async () => {
process.env['GITHUB_ENV'] = ''; // Stub out Environment file functionality so we can verify it writes to standard out (toolkit is backwards compatible) process.env['GITHUB_ENV'] = ''; // Stub out Environment file functionality so we can verify it writes to standard out (toolkit is backwards compatible)
@@ -59,17 +144,19 @@ describe('setup-go', () => {
// @actions/core // @actions/core
inputs = {}; inputs = {};
inSpy = jest.spyOn(core, 'getInput'); inSpy = core.getInput as jest.Mock<typeof core.getInput>;
inSpy.mockImplementation(name => inputs[name]); inSpy.mockImplementation(name => inputs[name]);
getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput'); getBooleanInputSpy = core.getBooleanInput as jest.Mock<
typeof core.getBooleanInput
>;
getBooleanInputSpy.mockImplementation(name => inputs[name]); getBooleanInputSpy.mockImplementation(name => inputs[name]);
exportVarSpy = jest.spyOn(core, 'exportVariable'); exportVarSpy = core.exportVariable as jest.Mock<typeof core.exportVariable>;
// node // node
os = {}; os = {};
platSpy = jest.spyOn(osm, 'platform'); platSpy = osm.platform as jest.Mock;
platSpy.mockImplementation(() => os['platform']); platSpy.mockImplementation(() => os['platform']);
archSpy = jest.spyOn(osm, 'arch'); archSpy = osm.arch as jest.Mock;
archSpy.mockImplementation(() => os['arch']); archSpy.mockImplementation(() => os['arch']);
execSpy = jest.spyOn(cp, 'execSync'); execSpy = jest.spyOn(cp, 'execSync');
execFileSpy = jest.spyOn(cp, 'execFileSync'); execFileSpy = jest.spyOn(cp, 'execFileSync');
@@ -78,7 +165,7 @@ describe('setup-go', () => {
}); });
// switch path join behaviour based on set os.platform // switch path join behaviour based on set os.platform
joinSpy = jest.spyOn(path, 'join'); joinSpy = path.join as jest.Mock<typeof path.join>;
joinSpy.mockImplementation((...paths: string[]): string => { joinSpy.mockImplementation((...paths: string[]): string => {
if (os['platform'] == 'win32') { if (os['platform'] == 'win32') {
return win32Join(...paths); return win32Join(...paths);
@@ -88,23 +175,22 @@ describe('setup-go', () => {
}); });
// @actions/tool-cache // @actions/tool-cache
findSpy = jest.spyOn(tc, 'find'); findSpy = tc.find as jest.Mock;
dlSpy = jest.spyOn(tc, 'downloadTool'); dlSpy = tc.downloadTool as jest.Mock;
extractTarSpy = jest.spyOn(tc, 'extractTar'); extractTarSpy = tc.extractTar as jest.Mock;
extractZipSpy = jest.spyOn(tc, 'extractZip'); extractZipSpy = tc.extractZip as jest.Mock;
cacheSpy = jest.spyOn(tc, 'cacheDir'); cacheSpy = tc.cacheDir as jest.Mock;
getSpy = jest.spyOn(im, 'getVersionsDist'); getSpy = vf.getVersionsDist as jest.Mock;
getManifestSpy = jest.spyOn(tc, 'getManifestFromRepo'); getManifestSpy = tc.getManifestFromRepo as jest.Mock;
getAllVersionsSpy = jest.spyOn(im, 'getManifest');
// httm // httm
httpmGetJsonSpy = jest.spyOn(httpm.HttpClient.prototype, 'getJson'); httpmGetJsonSpy = httpClientGetJson;
// io // io
whichSpy = jest.spyOn(io, 'which'); whichSpy = io.which as jest.Mock;
existsSpy = jest.spyOn(fs, 'existsSync'); existsSpy = jest.spyOn(fs, 'existsSync');
readFileSpy = jest.spyOn(fs, 'readFileSync'); readFileSpy = jest.spyOn(fs, 'readFileSync');
mkdirpSpy = jest.spyOn(io, 'mkdirP'); mkdirpSpy = io.mkdirP as jest.Mock;
// fs // fs
mkdirSpy = jest.spyOn(fs, 'mkdir'); mkdirSpy = jest.spyOn(fs, 'mkdir');
@@ -112,25 +198,18 @@ describe('setup-go', () => {
symlinkSpy.mockImplementation(() => {}); symlinkSpy.mockImplementation(() => {});
// gets // gets
getManifestSpy.mockImplementation(() => <tc.IToolRelease[]>goTestManifest); getManifestSpy.mockImplementation(() => goTestManifest as IToolRelease[]);
// writes // writes
cnSpy = jest.spyOn(process.stdout, 'write'); cnSpy = jest.spyOn(process.stdout, 'write');
logSpy = jest.spyOn(core, 'info'); logSpy = core.info as jest.Mock;
dbgSpy = jest.spyOn(core, 'debug'); setOutputSpy = core.setOutput as jest.Mock<typeof core.setOutput>;
getSpy.mockImplementation(() => <im.IGoVersion[] | null>goJsonData); warningSpy = core.warning as jest.Mock<typeof core.warning>;
cnSpy.mockImplementation(line => { dbgSpy = core.debug as jest.Mock;
// uncomment to debug getSpy.mockImplementation(() => goJsonData as IGoVersion[] | null);
// process.stderr.write('write:' + line + '\n'); cnSpy.mockImplementation(() => true);
}); logSpy.mockImplementation(() => {});
logSpy.mockImplementation(line => { dbgSpy.mockImplementation(() => {});
// uncomment to debug
//process.stderr.write('log:' + line + '\n');
});
dbgSpy.mockImplementation(msg => {
// uncomment to see debug output
// process.stderr.write(msg + '\n');
});
}); });
afterEach(() => { afterEach(() => {
@@ -138,6 +217,10 @@ describe('setup-go', () => {
delete process.env[im.GOTOOLCHAIN_ENV_VAR]; delete process.env[im.GOTOOLCHAIN_ENV_VAR];
delete process.env['GO_DOWNLOAD_BASE_URL']; delete process.env['GO_DOWNLOAD_BASE_URL'];
// reset the exit code that core.setFailed sets on the error-path tests so
// that a passing run does not leak a non-zero process exit code
process.exitCode = 0;
//jest.resetAllMocks(); //jest.resetAllMocks();
jest.clearAllMocks(); jest.clearAllMocks();
//jest.restoreAllMocks(); //jest.restoreAllMocks();
@@ -171,8 +254,10 @@ describe('setup-go', () => {
}); });
it('should return manifest from raw URL if repo fetch fails', async () => { it('should return manifest from raw URL if repo fetch fails', async () => {
getManifestSpy.mockRejectedValue(new Error('Fetch failed')); (getManifestSpy as jest.Mock<any>).mockRejectedValue(
httpmGetJsonSpy.mockResolvedValue({ new Error('Fetch failed')
);
(httpmGetJsonSpy as jest.Mock<any>).mockResolvedValue({
result: goTestManifest result: goTestManifest
}); });
const manifest = await im.getManifest(undefined); const manifest = await im.getManifest(undefined);
@@ -211,7 +296,7 @@ describe('setup-go', () => {
os.arch = 'x64'; os.arch = 'x64';
// spec: 1.13.0 => 1.13 // spec: 1.13.0 => 1.13
const match: im.IGoVersion | undefined = await im.findMatch('1.13.0'); const match: IGoVersion | undefined = await im.findMatch('1.13.0');
expect(match).toBeDefined(); expect(match).toBeDefined();
const version: string = match ? match.version : ''; const version: string = match ? match.version : '';
expect(version).toBe('go1.13'); expect(version).toBe('go1.13');
@@ -224,7 +309,7 @@ describe('setup-go', () => {
os.arch = 'x64'; os.arch = 'x64';
// spec: 1.13 => 1.13.7 (latest) // spec: 1.13 => 1.13.7 (latest)
const match: im.IGoVersion | undefined = await im.findMatch('1.13'); const match: IGoVersion | undefined = await im.findMatch('1.13');
expect(match).toBeDefined(); expect(match).toBeDefined();
const version: string = match ? match.version : ''; const version: string = match ? match.version : '';
expect(version).toBe('go1.13.7'); expect(version).toBe('go1.13.7');
@@ -237,7 +322,7 @@ describe('setup-go', () => {
os.arch = 'x64'; os.arch = 'x64';
// spec: ^1.13.6 => 1.13.7 // spec: ^1.13.6 => 1.13.7
const match: im.IGoVersion | undefined = await im.findMatch('^1.13.6'); const match: IGoVersion | undefined = await im.findMatch('^1.13.6');
expect(match).toBeDefined(); expect(match).toBeDefined();
const version: string = match ? match.version : ''; const version: string = match ? match.version : '';
expect(version).toBe('go1.13.7'); expect(version).toBe('go1.13.7');
@@ -250,7 +335,7 @@ describe('setup-go', () => {
os.arch = 'x32'; os.arch = 'x32';
// spec: 1 => 1.13.7 (latest) // spec: 1 => 1.13.7 (latest)
const match: im.IGoVersion | undefined = await im.findMatch('1'); const match: IGoVersion | undefined = await im.findMatch('1');
expect(match).toBeDefined(); expect(match).toBeDefined();
const version: string = match ? match.version : ''; const version: string = match ? match.version : '';
expect(version).toBe('go1.13.7'); expect(version).toBe('go1.13.7');
@@ -263,7 +348,7 @@ describe('setup-go', () => {
os.arch = 'x64'; os.arch = 'x64';
// spec: 1.14, stable=false => go1.14rc1 // spec: 1.14, stable=false => go1.14rc1
const match: im.IGoVersion | undefined = await im.findMatch('1.14.0-rc.1'); const match: IGoVersion | undefined = await im.findMatch('1.14.0-rc.1');
expect(match).toBeDefined(); expect(match).toBeDefined();
const version: string = match ? match.version : ''; const version: string = match ? match.version : '';
expect(version).toBe('go1.14rc1'); expect(version).toBe('go1.14rc1');
@@ -568,6 +653,106 @@ describe('setup-go', () => {
expect(added).toBeTruthy(); expect(added).toBeTruthy();
}); });
describe('go env outputs', () => {
const goEnv = {
GOPATH: '/Users/testuser/go',
GOBIN: '',
GOROOT: '/usr/local/go',
GOCACHE: '/Users/testuser/.cache/go-build',
GOMODCACHE: '/Users/testuser/go/pkg/mod',
GOOS: 'darwin',
GOARCH: 'arm64',
GOTOOLDIR: '/usr/local/go/pkg/tool/darwin_arm64'
};
it('parses the output of go env -json', () => {
execFileSpy.mockImplementation(() => JSON.stringify(goEnv));
expect(main.readGoEnv('/usr/local/go/bin/go')).toEqual(goEnv);
});
it('returns undefined when go env -json is not supported', () => {
execFileSpy.mockImplementation(() => {
throw new Error('flag provided but not defined: -json');
});
expect(main.readGoEnv('/usr/local/go/bin/go')).toBeUndefined();
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining('flag provided but not defined: -json')
);
expect(warningSpy).not.toHaveBeenCalled();
});
it('returns undefined when the output is not valid JSON', () => {
execFileSpy.mockImplementation(() => 'GOPATH="/Users/testuser/go"');
expect(main.readGoEnv('/usr/local/go/bin/go')).toBeUndefined();
});
it('returns undefined when the output is not a JSON object', () => {
execFileSpy.mockImplementation(() => '[]');
expect(main.readGoEnv('/usr/local/go/bin/go')).toBeUndefined();
});
it('sets an output for each exposed variable', () => {
main.setGoEnvOutputs(goEnv);
expect(GO_ENV_OUTPUTS).toEqual([
'GOPATH',
'GOBIN',
'GOROOT',
'GOCACHE',
'GOMODCACHE',
'GOOS',
'GOARCH',
'GOTOOLDIR'
]);
for (const name of GO_ENV_OUTPUTS) {
expect(setOutputSpy).toHaveBeenCalledWith(name, goEnv[name] ?? '');
}
});
it('falls back to $GOPATH/bin when GOBIN is empty', () => {
main.setGoEnvOutputs(goEnv);
expect(setOutputSpy).toHaveBeenCalledWith(
'go-bin-path',
'/Users/testuser/go/bin'
);
});
it('prefers GOBIN over $GOPATH/bin when it is set', () => {
main.setGoEnvOutputs({...goEnv, GOBIN: '/Users/testuser/bin'});
expect(setOutputSpy).toHaveBeenCalledWith(
'go-bin-path',
'/Users/testuser/bin'
);
});
it('uses the first entry of a multi-entry GOPATH', () => {
const GOPATH = ['/Users/testuser/go', '/Users/testuser/other'].join(
path.delimiter
);
main.setGoEnvOutputs({...goEnv, GOPATH});
expect(setOutputSpy).toHaveBeenCalledWith('GOPATH', GOPATH);
expect(setOutputSpy).toHaveBeenCalledWith(
'go-bin-path',
'/Users/testuser/go/bin'
);
});
it('leaves variables missing from go env -json empty', () => {
main.setGoEnvOutputs({});
expect(setOutputSpy).toHaveBeenCalledWith('GOPATH', '');
expect(setOutputSpy).toHaveBeenCalledWith('go-bin-path', '');
});
});
interface Annotation { interface Annotation {
file: string; file: string;
line: number; line: number;
@@ -817,10 +1002,9 @@ describe('setup-go', () => {
getManifestSpy.mockImplementation(() => { getManifestSpy.mockImplementation(() => {
throw new Error('Unable to download manifest'); throw new Error('Unable to download manifest');
}); });
httpmGetJsonSpy.mockRejectedValue( (httpmGetJsonSpy as jest.Mock<any>).mockRejectedValue(
new Error('Unable to download manifest from raw URL') new Error('Unable to download manifest from raw URL')
); );
getAllVersionsSpy.mockImplementationOnce(() => undefined);
dlSpy.mockImplementation(async () => '/some/temp/path'); dlSpy.mockImplementation(async () => '/some/temp/path');
const toolPath = path.normalize('/cache/go/1.13.7/x64'); const toolPath = path.normalize('/cache/go/1.13.7/x64');

View File

@@ -1,4 +1,5 @@
import {isSelfHosted} from '../src/utils'; import {describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import {isSelfHosted} from '../src/utils.js';
describe('utils', () => { describe('utils', () => {
describe('isSelfHosted', () => { describe('isSelfHosted', () => {

View File

@@ -1,38 +1,57 @@
import fs from 'fs'; import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import * as io from '@actions/io';
import * as tc from '@actions/tool-cache';
import path from 'path'; import path from 'path';
const statSyncMock = jest.fn(() => ({
isDirectory: () => true,
isFile: () => true
}));
const readdirSyncMock = jest.fn(() => [] as any);
const writeFileSyncMock = jest.fn();
const mkdirMock = jest.fn();
const symlinkSyncMock = jest.fn();
jest.unstable_mockModule('@actions/io', () => ({
which: jest.fn(),
mkdirP: jest.fn(() => Promise.resolve()),
rmRF: jest.fn(() => Promise.resolve()),
mv: jest.fn(() => Promise.resolve()),
cp: jest.fn(() => Promise.resolve())
}));
const realFs = (await import('fs')).default;
const fsExports = {
...realFs,
statSync: statSyncMock,
readdirSync: readdirSyncMock,
writeFileSync: writeFileSyncMock,
mkdir: mkdirMock,
symlinkSync: symlinkSyncMock
};
jest.unstable_mockModule('fs', () => ({...fsExports, default: fsExports}));
const io = await import('@actions/io');
const tc = await import('@actions/tool-cache');
describe('Windows performance workaround', () => { describe('Windows performance workaround', () => {
let mkdirSpy: jest.SpyInstance; let rmRFSpy: jest.Mock;
let symlinkSpy: jest.SpyInstance; let mkdirPSpy: jest.Mock;
let statSpy: jest.SpyInstance; let cpSpy: jest.Mock;
let readdirSpy: jest.SpyInstance;
let writeFileSpy: jest.SpyInstance;
let rmRFSpy: jest.SpyInstance;
let mkdirPSpy: jest.SpyInstance;
let cpSpy: jest.SpyInstance;
let runnerToolCache: string | undefined; let runnerToolCache: string | undefined;
beforeEach(() => { beforeEach(() => {
mkdirSpy = jest.spyOn(fs, 'mkdir'); rmRFSpy = io.rmRF as jest.Mock;
symlinkSpy = jest.spyOn(fs, 'symlinkSync'); mkdirPSpy = io.mkdirP as jest.Mock;
statSpy = jest.spyOn(fs, 'statSync'); cpSpy = io.cp as jest.Mock;
readdirSpy = jest.spyOn(fs, 'readdirSync');
writeFileSpy = jest.spyOn(fs, 'writeFileSync');
rmRFSpy = jest.spyOn(io, 'rmRF');
mkdirPSpy = jest.spyOn(io, 'mkdirP');
cpSpy = jest.spyOn(io, 'cp');
// default implementations // default implementations
// @ts-ignore - not implement unused methods statSyncMock.mockImplementation(() => ({
statSpy.mockImplementation(() => ({ isDirectory: () => true,
isDirectory: () => true isFile: () => true
})); }));
readdirSpy.mockImplementation(() => []); readdirSyncMock.mockImplementation(() => []);
writeFileSpy.mockImplementation(() => {}); writeFileSyncMock.mockImplementation(() => {});
mkdirSpy.mockImplementation(() => {}); mkdirMock.mockImplementation(() => {});
symlinkSpy.mockImplementation(() => {}); symlinkSyncMock.mockImplementation(() => {});
rmRFSpy.mockImplementation(() => Promise.resolve()); rmRFSpy.mockImplementation(() => Promise.resolve());
mkdirPSpy.mockImplementation(() => Promise.resolve()); mkdirPSpy.mockImplementation(() => Promise.resolve());
cpSpy.mockImplementation(() => Promise.resolve()); cpSpy.mockImplementation(() => Promise.resolve());

View File

@@ -25,7 +25,25 @@ outputs:
go-version: go-version:
description: 'The installed Go version. Useful when given a version range as input.' description: 'The installed Go version. Useful when given a version range as input.'
cache-hit: cache-hit:
description: 'A boolean value to indicate if a cache was hit' description: 'A boolean value indicating whether both module and build cache entries were restored on their primary keys'
GOPATH:
description: 'The value of `go env GOPATH`.'
GOBIN:
description: 'The value of `go env GOBIN`.'
go-bin-path:
description: 'The base Go binary directory: `GOBIN`, otherwise `bin` under the first `GOPATH` entry.'
GOROOT:
description: 'The value of `go env GOROOT`.'
GOCACHE:
description: 'The value of `go env GOCACHE`, the build cache directory.'
GOMODCACHE:
description: 'The value of `go env GOMODCACHE`, the module cache directory.'
GOOS:
description: 'The value of `go env GOOS`, the target OS in Go notation (linux, darwin, windows).'
GOARCH:
description: 'The value of `go env GOARCH`, the target architecture in Go notation (amd64, arm64).'
GOTOOLDIR:
description: 'The value of `go env GOTOOLDIR`, the directory holding the toolchain binaries.'
runs: runs:
using: 'node24' using: 'node24'
main: 'dist/setup/index.js' main: 'dist/setup/index.js'

141662
dist/cache-save/index.js vendored

File diff suppressed because one or more lines are too long

3
dist/cache-save/package.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

148377
dist/setup/index.js vendored

File diff suppressed because one or more lines are too long

3
dist/setup/package.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -12,6 +12,9 @@
- [Restore-only caches](advanced-usage.md#restore-only-caches) - [Restore-only caches](advanced-usage.md#restore-only-caches)
- [Parallel builds](advanced-usage.md#parallel-builds) - [Parallel builds](advanced-usage.md#parallel-builds)
- [Outputs](advanced-usage.md#outputs) - [Outputs](advanced-usage.md#outputs)
- [go-version](advanced-usage.md#go-version)
- [cache-hit](advanced-usage.md#cache-hit)
- [Go environment outputs](advanced-usage.md#go-environment-outputs)
- [Custom download URL](advanced-usage.md#custom-download-url) - [Custom download URL](advanced-usage.md#custom-download-url)
- [Using `setup-go` on GHES](advanced-usage.md#using-setup-go-on-ghes) - [Using `setup-go` on GHES](advanced-usage.md#using-setup-go-on-ghes)
@@ -23,8 +26,8 @@ For repeatable builds, specify the **exact major, minor, and patch version** (su
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: '1.25.5' go-version: '1.25.5'
- run: go run hello.go - run: go run hello.go
@@ -36,8 +39,8 @@ You can specify **only a major and minor version** if you are okay with the most
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: '1.25' go-version: '1.25'
- run: go run hello.go - run: go run hello.go
@@ -53,8 +56,8 @@ Download and use beta or RC Go versions as needed.
```yaml ```yaml
# RC version # RC version
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: '1.25.0-rc.2' go-version: '1.25.0-rc.2'
- run: go version - run: go version
@@ -63,8 +66,8 @@ steps:
```yaml ```yaml
# Beta version # Beta version
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: '1.19.0-beta.1' go-version: '1.19.0-beta.1'
- run: go version - run: go version
@@ -80,8 +83,8 @@ If `oldstable` is provided, the action resolves it to the latest patch release o
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: 'stable' go-version: 'stable'
- run: go run hello.go - run: go run hello.go
@@ -89,8 +92,8 @@ steps:
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: 'oldstable' go-version: 'oldstable'
- run: go run hello.go - run: go run hello.go
@@ -100,8 +103,8 @@ You can also use **SemVer's version range syntax**, for instance:
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: '^1.25.1' go-version: '^1.25.1'
- run: go version - run: go version
@@ -109,8 +112,8 @@ steps:
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: '>=1.24.0-rc.1' go-version: '>=1.24.0-rc.1'
- run: go version - run: go version
@@ -129,9 +132,9 @@ jobs:
go: [ '1.24', '1.25' ] go: [ '1.24', '1.25' ]
name: Go ${{ matrix.go }} sample name: Go ${{ matrix.go }} sample
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Setup go - name: Setup go
uses: actions/setup-go@v6 uses: actions/setup-go@v7
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- run: go run hello.go - run: go run hello.go
@@ -154,9 +157,9 @@ jobs:
- os: windows-latest - os: windows-latest
go: '1.25' go: '1.25'
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Setup go - name: Setup go
uses: actions/setup-go@v6 uses: actions/setup-go@v7
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- run: go run hello.go - run: go run hello.go
@@ -170,8 +173,8 @@ If both the `go-version` and the `go-version-file` inputs are provided then the
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version-file: 'path/to/go.mod' # Read Go version from go.mod go-version-file: 'path/to/go.mod' # Read Go version from go.mod
- run: go version - run: go version
@@ -187,8 +190,8 @@ steps:
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version-file: '.go-version' # Read Go version from .go-version go-version-file: '.go-version' # Read Go version from .go-version
- run: go version - run: go version
@@ -196,8 +199,8 @@ steps:
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version-file: '.tool-versions' # Read Go version from .tool-versions go-version-file: '.tool-versions' # Read Go version from .tool-versions
- run: go version - run: go version
@@ -205,8 +208,8 @@ steps:
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version-file: 'go.work' # Read Go version from go.work go-version-file: 'go.work' # Read Go version from go.work
- run: go version - run: go version
@@ -228,8 +231,8 @@ want the most up-to-date Go version to always be used. It supports major (e.g.,
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: '1.25' go-version: '1.25'
check-latest: true check-latest: true
@@ -238,12 +241,21 @@ steps:
## Caching ## Caching
The action restores and saves two independent cache entries:
- **Module sources (`GOMODCACHE`)**: keyed by the runner OS and dependency-file hash. These sources can be reused across Go versions, runner architectures, and runner images on the same OS.
- **Build outputs (`GOCACHE`)**: keyed by the runner OS, runner architecture, Linux runner image (when applicable), Go version, and dependency-file hash.
Both entries use the hash of `cache-dependency-path`, or the root `go.mod` by default. Adding source files or build settings to that input invalidates both entries when those files change. Custom cache locations and the cache service's normal access restrictions still apply.
Each entry is restored and saved independently, with concurrent transfers where safe. An exact hit is not uploaded again, even when the other entry misses. A failure for one entry does not prevent caching the other. The `cache-hit` output is `true` only when both entries are restored on their primary keys.
### Caching in monorepos ### Caching in monorepos
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: '1.25' go-version: '1.25'
cache-dependency-path: subdir/go.sum cache-dependency-path: subdir/go.sum
@@ -256,8 +268,8 @@ steps:
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: '1.25' go-version: '1.25'
cache-dependency-path: '**/go.sum' cache-dependency-path: '**/go.sum'
@@ -266,8 +278,8 @@ steps:
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: '1.25' go-version: '1.25'
cache-dependency-path: | cache-dependency-path: |
@@ -278,7 +290,7 @@ steps:
### Multi-target builds ### Multi-target builds
`cache-dependency-path` isn’t limited to dependency files (like `go.sum`). It can also include files that capture build settings (for example, `GOOS`/`GOARCH`). This allows separate caches per target platform (OS/architecture) and helps avoid reusing caches across incompatible builds. `GOOS` and `GOARCH` are not automatically included in the cache keys. `cache-dependency-path` isn’t limited to dependency files (like `go.sum`); it can also include files that capture these build settings. This prevents jobs for different targets from competing to populate the same immutable cache entry. Because both entries use the same dependency-file hash, this workaround separates the module cache as well as the build cache.
```yaml ```yaml
env: env:
@@ -286,16 +298,15 @@ env:
GOARCH: ... GOARCH: ...
steps: steps:
- uses: actions/checkout@v7
- run: echo "$GOOS $GOARCH" > env.txt - run: echo "$GOOS $GOARCH" > env.txt
- uses: actions/setup-go@v7
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with: with:
go-version: '1.25' go-version: '1.25'
cache-dependency-path: | cache-dependency-path: |
go.sum go.sum
env.txt env.txt
- run: go run hello.go - run: go build ./...
``` ```
### Cache invalidation on source changes ### Cache invalidation on source changes
@@ -306,8 +317,8 @@ Besides dependencies, the action can also cache build outputs (the [`GOCACHE`](h
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: '1.25' go-version: '1.25'
cache-dependency-path: | cache-dependency-path: |
@@ -328,10 +339,10 @@ jobs:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Setup go - name: Setup go
id: setup-go id: setup-go
uses: actions/setup-go@v6 uses: actions/setup-go@v7
with: with:
go-version: '1.25.5' go-version: '1.25.5'
cache: false cache: false
@@ -362,20 +373,26 @@ jobs:
if: runner.os == 'Linux' if: runner.os == 'Linux'
shell: bash shell: bash
run: echo "CACHE_OS_SUFFIX=$ImageOS-" >> $GITHUB_ENV run: echo "CACHE_OS_SUFFIX=$ImageOS-" >> $GITHUB_ENV
- name: Restore Go cache - name: Restore Go module cache
id: go-cache id: go-module-cache
uses: actions/cache/restore@v5 uses: actions/cache/restore@v5
with: with:
path: | path: ${{ env.GO_MOD_CACHE }}
${{ env.GO_MOD_CACHE }} key: setup-go-modules-${{ runner.os }}-${{ hashFiles('go.mod') }}
${{ env.GO_BUILD_CACHE }} - name: Restore Go build cache
key: setup-go-${{ runner.os }}-${{ env.ARCH }}-${{ env.CACHE_OS_SUFFIX }}go-${{ steps.setup-go.outputs.go-version }}-${{ hashFiles('**/go.mod') }} id: go-build-cache
uses: actions/cache/restore@v5
with:
path: ${{ env.GO_BUILD_CACHE }}
key: setup-go-build-${{ runner.os }}-${{ env.ARCH }}-${{ env.CACHE_OS_SUFFIX }}go-${{ steps.setup-go.outputs.go-version }}-${{ hashFiles('go.mod') }}
- name: Download modules - name: Download modules
run: go mod download run: go mod download
- name: Build - name: Build
run: go build ./... run: go build ./...
``` ```
Use the same file patterns in `hashFiles` as the `cache-dependency-path` of the workflow that saves the caches. The example above matches the default root `go.mod` input.
> If there are several builds on the same repo, it may make sense to create a cache in one build and use it in others. The action [actions/cache/restore](https://github.com/actions/cache/tree/main/restore#only-restore-cache) > If there are several builds on the same repo, it may make sense to create a cache in one build and use it in others. The action [actions/cache/restore](https://github.com/actions/cache/tree/main/restore#only-restore-cache)
should be used in this case. should be used in this case.
@@ -394,8 +411,8 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
id: go124 id: go124
with: with:
go-version: '^1.24' go-version: '^1.24'
@@ -404,22 +421,52 @@ jobs:
### `cache-hit` ### `cache-hit`
**cache-hit** output is available with a boolean value that indicates whether a cache hit occurred on the primary key: **cache-hit** is `true` only when both the module cache and the build cache are restored on their primary keys. If only one entry hits its primary key, the output is `false`, but that entry is still used and is not saved again. An entry restored from a non-primary key can still be saved under its primary key:
```yaml ```yaml
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
id: go124 id: go124
with: with:
go-version: '1.24' go-version: '1.24'
cache: true cache: true
- run: echo "Was the Go cache restored? ${{ steps.go124.outputs.cache-hit }}" # true if cache-hit occurred - run: echo "Were both Go caches restored? ${{ steps.go124.outputs.cache-hit }}"
``` ```
### Go environment outputs
Once Go is on the `PATH`, the action exposes the most commonly needed `go env` variables as outputs, so workflows don't have to query them in duplicated `bash`/`pwsh` steps — one expression works on Linux, macOS, and Windows. The values are a snapshot from setup time: anything a later step changes, such as setting `GOOS`/`GOARCH` to cross-compile, is not reflected.
| Output | Notes |
| --- | --- |
| `GOPATH` | Go workspace root; `bin` and `pkg/mod` live under it |
| `GOBIN` | Go 1.27+ reports an implicit `$GOPATH/bin` default; earlier releases are empty unless `GOBIN` is set |
| `go-bin-path` | The base Go binary directory (`GOBIN` or `$GOPATH/bin`), using the first `GOPATH` entry. Cross-compiled binaries go one level deeper, in `$GOPATH/bin/$GOOS_$GOARCH` |
| `GOROOT` | Installation directory of the Go toolchain in use |
| `GOCACHE` | Build cache directory |
| `GOMODCACHE` | Module cache directory |
| `GOOS` | Go notation (`linux`, `darwin`, `windows`), unlike `runner.os` |
| `GOARCH` | Go notation (`amd64`, `arm64`), unlike `runner.arch` or the `x64` in the action's cache key |
| `GOTOOLDIR` | Holds `compile`, `link`, `vet` and the other toolchain binaries |
```yaml
steps:
- uses: actions/setup-go@v7
id: setup-go
with:
go-version: '1.25.5'
- run: echo "Modules are cached in ${{ steps.setup-go.outputs.GOMODCACHE }}"
```
Quote `go-bin-path` in shell commands, since it may contain spaces. Variables without a dedicated output are not exposed; run `go env <NAME>` in a step when you need one.
> [!NOTE]
> These outputs need Go 1.9 or newer (`go env -json`); on older releases the action logs a message, leaves them unset and does not fail. `GOCACHE` needs Go 1.10 and `GOMODCACHE` needs Go 1.15.
## Custom download URL ## Custom download URL
The `go-download-base-url` input lets you download Go from a mirror or alternative source instead of the default `https://go.dev/dl`. This can also be set via the `GO_DOWNLOAD_BASE_URL` environment variable; the input takes precedence over the environment variable. The `go-download-base-url` input lets you download Go from a mirror or alternative source instead of the default `https://go.dev/dl`. This can also be set via the `GO_DOWNLOAD_BASE_URL` environment variable; the input takes precedence over the environment variable.
@@ -430,8 +477,8 @@ When a custom base URL is provided, the action skips the `actions/go-versions` m
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: '1.25' go-version: '1.25'
go-download-base-url: 'https://aka.ms/golang/release/latest' go-download-base-url: 'https://aka.ms/golang/release/latest'
@@ -445,8 +492,8 @@ env:
GO_DOWNLOAD_BASE_URL: 'https://aka.ms/golang/release/latest' GO_DOWNLOAD_BASE_URL: 'https://aka.ms/golang/release/latest'
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: '1.25' go-version: '1.25'
- run: go version - run: go version
@@ -462,8 +509,8 @@ If your custom download source requires authentication, the `token` input is for
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@v7
with: with:
go-version: '1.25' go-version: '1.25'
go-download-base-url: 'https://private-mirror.example.com/golang' go-download-base-url: 'https://private-mirror.example.com/golang'
@@ -485,7 +532,7 @@ If that fails as well the action will try to download versions directly from [go
If that fails as well you can get a higher rate limit with [generating a personal access token on github.com](https://github.com/settings/tokens/new) and passing it as the `token` input to the action: If that fails as well you can get a higher rate limit with [generating a personal access token on github.com](https://github.com/settings/tokens/new) and passing it as the `token` input to the action:
```yaml ```yaml
uses: actions/setup-go@v6 uses: actions/setup-go@v7
with: with:
token: ${{ secrets.GH_DOTCOM_TOKEN }} token: ${{ secrets.GH_DOTCOM_TOKEN }}
go-version: '1.25' go-version: '1.25'

74
eslint.config.mjs Normal file
View File

@@ -0,0 +1,74 @@
// This is a reusable configuration file copied from https://github.com/actions/reusable-workflows/tree/main/reusable-configurations. Please don't make changes to this file as it's the subject of an automatic update.
import js from '@eslint/js';
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import jest from 'eslint-plugin-jest';
import n from 'eslint-plugin-n';
import prettier from 'eslint-config-prettier';
import globals from 'globals';
export default [
{
ignores: ['**/*', '!src/**', '!__tests__/**']
},
js.configs.recommended,
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
ecmaVersion: 2022,
sourceType: 'module',
globals: {
...globals.node,
...globals.es2015
}
},
plugins: {
'@typescript-eslint': tsPlugin,
n
},
rules: {
...tsPlugin.configs.recommended.rules,
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-ignore': 'allow-with-description'
}
],
'no-console': 'error',
yoda: 'error',
'prefer-const': [
'error',
{
destructuring: 'all'
}
],
'no-control-regex': 'off',
'no-constant-condition': ['error', {checkLoops: false}],
'no-undef': 'off',
'no-useless-assignment': 'off',
'n/no-extraneous-import': 'error'
}
},
{
files: ['**/*{test,spec}.ts'],
plugins: {jest},
languageOptions: {
globals: {
...globals.jest
}
},
rules: {
...jest.configs['flat/recommended'].rules,
'@typescript-eslint/no-unused-vars': 'off',
'jest/no-standalone-expect': 'off',
'jest/no-conditional-expect': 'off',
'no-console': 'off'
}
},
prettier
];

View File

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

24
jest.config.ts Normal file
View File

@@ -0,0 +1,24 @@
export default {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
roots: ['<rootDir>'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': [
'ts-jest',
{
useESM: true,
diagnostics: {
ignoreCodes: [151002]
}
}
]
},
extensionsToTreatAsEsm: ['.ts'],
transformIgnorePatterns: ['node_modules/(?!(@actions)/)'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1'
},
verbose: true
};

4970
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
{ {
"name": "setup-go", "name": "setup-go",
"version": "6.2.0", "version": "7.0.0",
"type": "module",
"private": true, "private": true,
"description": "setup go action", "description": "setup go action",
"main": "lib/setup-go.js", "main": "lib/setup-go.js",
@@ -9,11 +10,11 @@
}, },
"scripts": { "scripts": {
"build": "tsc && ncc build -o dist/setup src/setup-go.ts && ncc build -o dist/cache-save src/cache-save.ts", "build": "tsc && ncc build -o dist/setup src/setup-go.ts && ncc build -o dist/cache-save src/cache-save.ts",
"format": "prettier --no-error-on-unmatched-pattern --config ./.prettierrc.js --write \"**/*.{ts,yml,yaml}\"", "format": "prettier --no-error-on-unmatched-pattern --write \"**/*.{ts,yml,yaml}\"",
"format-check": "prettier --no-error-on-unmatched-pattern --config ./.prettierrc.js --check \"**/*.{ts,yml,yaml}\"", "format-check": "prettier --no-error-on-unmatched-pattern --check \"**/*.{ts,yml,yaml}\"",
"lint": "eslint --config ./.eslintrc.js \"**/*.ts\"", "lint": "eslint \"**/*.ts\"",
"lint:fix": "eslint --config ./.eslintrc.js \"**/*.ts\" --fix", "lint:fix": "eslint \"**/*.ts\" --fix",
"test": "jest --coverage", "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --runInBand --coverage",
"pre-checkin": "npm run format && npm run lint:fix && npm run build && npm test" "pre-checkin": "npm run format && npm run lint:fix && npm run build && npm test"
}, },
"repository": { "repository": {
@@ -28,31 +29,32 @@
"author": "GitHub", "author": "GitHub",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/cache": "^5.0.1", "@actions/cache": "^6.3.0",
"@actions/core": "^1.11.1", "@actions/core": "^3.0.1",
"@actions/exec": "^1.1.1", "@actions/exec": "^3.0.0",
"@actions/glob": "^0.5.0", "@actions/glob": "^0.7.0",
"@actions/http-client": "^2.2.1", "@actions/http-client": "^4.0.1",
"@actions/io": "^1.0.2", "@actions/io": "^3.0.2",
"@actions/tool-cache": "^2.0.2", "@actions/tool-cache": "^4.0.0",
"semver": "^7.7.3" "semver": "^7.8.5"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.14", "@eslint/js": "^10.0.1",
"@types/node": "^24.1.0", "@jest/globals": "^30.4.1",
"@types/semver": "^7.7.1", "@types/node": "^24.0.0",
"@typescript-eslint/eslint-plugin": "^8.31.1", "@types/semver": "^7.7.0",
"@typescript-eslint/parser": "^8.35.1", "@typescript-eslint/eslint-plugin": "^8.62.0",
"@vercel/ncc": "^0.38.1", "@typescript-eslint/parser": "^8.62.0",
"eslint": "^8.57.0", "@vercel/ncc": "^0.44.0",
"eslint-config-prettier": "^10.1.8", "eslint": "^10.5.0",
"eslint-plugin-jest": "^29.0.1", "eslint-config-prettier": "^10.0.0",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-jest": "^29.15.2",
"jest": "^29.7.0", "eslint-plugin-n": "^18.1.0",
"jest-circus": "^29.7.0", "globals": "^17.7.0",
"nock": "^10.0.6", "jest": "^30.4.2",
"prettier": "^2.8.4", "nock": "^14.0.0",
"ts-jest": "^29.3.2", "prettier": "^3.8.4",
"typescript": "^5.8.3" "ts-jest": "^29.4.11",
"typescript": "^6.0.3"
} }
} }

View File

@@ -4,15 +4,21 @@ import * as glob from '@actions/glob';
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
import {State, Outputs} from './constants'; import {State, Outputs} from './constants.js';
import {PackageManagerInfo} from './package-managers'; import {PackageManagerInfo} from './package-managers.js';
import {getCacheDirectoryPath, getPackageManagerInfo} from './cache-utils'; import {
CacheEntry,
canRestoreCachesInParallel,
getCacheDirectoryPath,
getPackageManagerInfo
} from './cache-utils.js';
export const restoreCache = async ( export const restoreCache = async (
versionSpec: string, versionSpec: string,
packageManager: string, packageManager: string,
cacheDependencyPath?: string cacheDependencyPath?: string
) => { ) => {
core.setOutput(Outputs.CacheHit, false);
const packageManagerInfo = await getPackageManagerInfo(packageManager); const packageManagerInfo = await getPackageManagerInfo(packageManager);
const platform = process.env.RUNNER_OS; const platform = process.env.RUNNER_OS;
const arch = process.arch; const arch = process.arch;
@@ -32,22 +38,51 @@ export const restoreCache = async (
const linuxVersion = const linuxVersion =
process.env.RUNNER_OS === 'Linux' ? `${process.env.ImageOS}-` : ''; process.env.RUNNER_OS === 'Linux' ? `${process.env.ImageOS}-` : '';
const primaryKey = `setup-go-${platform}-${arch}-${linuxVersion}go-${versionSpec}-${fileHash}`; const primaryKeys = {
core.debug(`primary key is ${primaryKey}`); // Downloaded module sources can be reused across Go versions and runner
// architectures. Keep OS isolation for filesystem compatibility.
modules: `setup-go-modules-${platform}-${fileHash}`,
build: `setup-go-build-${platform}-${arch}-${linuxVersion}go-${versionSpec}-${fileHash}`
};
const entries: CacheEntry[] = cachePaths.map(directory => ({
...directory,
primaryKey: primaryKeys[directory.kind]
}));
core.saveState(State.CachePrimaryKey, primaryKey); const restoreEntry = async (entry: CacheEntry) => {
core.debug(`${entry.kind} primary key is ${entry.primaryKey}`);
try {
entry.matchedKey = await cache.restoreCache(
[entry.path],
entry.primaryKey
);
if (entry.matchedKey) {
core.info(`${entry.kind} cache restored from key: ${entry.matchedKey}`);
} else {
core.info(`${entry.kind} cache is not found`);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
core.warning(`Restore ${entry.kind} cache failed: ${message}`);
}
};
const cacheKey = await cache.restoreCache(cachePaths, primaryKey); if (canRestoreCachesInParallel(entries)) {
core.setOutput(Outputs.CacheHit, Boolean(cacheKey)); await Promise.all(entries.map(restoreEntry));
} else {
if (!cacheKey) { for (const entry of entries) {
core.info(`Cache is not found`); await restoreEntry(entry);
core.setOutput(Outputs.CacheHit, false); }
return;
} }
core.saveState(State.CacheMatchedKey, cacheKey); // Preserve the resolved paths so the post step uses the same cache versions,
core.info(`Cache restored from key: ${cacheKey}`); // even if a later step changes the Go environment.
core.saveState(State.CacheEntries, entries);
core.setOutput(
Outputs.CacheHit,
entries.length === packageManagerInfo.cacheFolderCommandList.length &&
entries.every(entry => entry.matchedKey === entry.primaryKey)
);
}; };
const findDependencyFile = (packageManager: PackageManagerInfo) => { const findDependencyFile = (packageManager: PackageManagerInfo) => {

View File

@@ -1,8 +1,8 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as cache from '@actions/cache'; import * as cache from '@actions/cache';
import fs from 'fs'; import fs from 'fs';
import {State} from './constants'; import {State} from './constants.js';
import {getCacheDirectoryPath, getPackageManagerInfo} from './cache-utils'; import type {CacheEntry} from './cache-utils.js';
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in // Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to // @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
@@ -39,48 +39,48 @@ export async function run(earlyExit?: boolean) {
} }
const cachePackages = async () => { const cachePackages = async () => {
const packageManager = 'default'; const entries: CacheEntry[] = JSON.parse(
core.getState(State.CacheEntries) || '[]'
const state = core.getState(State.CacheMatchedKey);
const primaryKey = core.getState(State.CachePrimaryKey);
const packageManagerInfo = await getPackageManagerInfo(packageManager);
const cachePaths = await getCacheDirectoryPath(packageManagerInfo);
const nonExistingPaths = cachePaths.filter(
cachePath => !fs.existsSync(cachePath)
); );
if (!entries.length) {
if (nonExistingPaths.length === cachePaths.length) {
core.warning('There are no cache folders on the disk');
return;
}
if (nonExistingPaths.length) {
logWarning(
`Cache folder path is retrieved but doesn't exist on disk: ${nonExistingPaths.join(
', '
)}`
);
}
if (!primaryKey) {
core.info( core.info(
'Primary key was not generated. Please check the log messages above for more errors or information' 'Cache keys were not generated. Please check the log messages above for more errors or information'
); );
return; return;
} }
if (primaryKey === state) { await Promise.all(
entries.map(async entry => {
try {
await saveCacheEntry(entry);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
core.warning(`Save ${entry.kind} cache failed: ${message}`);
}
})
);
};
const saveCacheEntry = async (entry: CacheEntry) => {
const {kind, path, primaryKey, matchedKey} = entry;
if (primaryKey === matchedKey) {
core.info( core.info(
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.` `Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
); );
return; return;
} }
const cacheId = await cache.saveCache(cachePaths, primaryKey); if (!fs.existsSync(path)) {
logWarning(`${kind} cache folder doesn't exist on disk: ${path}`);
return;
}
const cacheId = await cache.saveCache([path], primaryKey);
if (cacheId === -1) { if (cacheId === -1) {
// saveCache returns -1 without throwing when the cache was not saved, e.g.
// a reserve collision or a read-only token (fork PR). @actions/cache has
// already logged the reason at the appropriate severity, so just trace it.
core.debug(`Cache was not saved for the key: ${primaryKey}`);
return; return;
} }
core.info(`Cache saved with the key: ${primaryKey}`); core.info(`Cache saved with the key: ${primaryKey}`);

View File

@@ -1,7 +1,23 @@
import * as cache from '@actions/cache'; import * as cache from '@actions/cache';
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as exec from '@actions/exec'; import * as exec from '@actions/exec';
import {supportedPackageManagers, PackageManagerInfo} from './package-managers'; import fs from 'fs';
import path from 'path';
import {
supportedPackageManagers,
PackageManagerInfo,
CacheKind
} from './package-managers.js';
export interface CacheDirectory {
kind: CacheKind;
path: string;
}
export interface CacheEntry extends CacheDirectory {
primaryKey: string;
matchedKey?: string;
}
export const getCommandOutput = async (toolCommand: string) => { export const getCommandOutput = async (toolCommand: string) => {
let {stdout, stderr, exitCode} = await exec.getExecOutput( let {stdout, stderr, exitCode} = await exec.getExecOutput(
@@ -33,24 +49,24 @@ export const getPackageManagerInfo = async (packageManager: string) => {
export const getCacheDirectoryPath = async ( export const getCacheDirectoryPath = async (
packageManagerInfo: PackageManagerInfo packageManagerInfo: PackageManagerInfo
) => { ): Promise<CacheDirectory[]> => {
const pathOutputs = await Promise.allSettled( const pathOutputs = await Promise.allSettled(
packageManagerInfo.cacheFolderCommandList.map(async command => packageManagerInfo.cacheFolderCommandList.map(async ({kind, command}) => ({
getCommandOutput(command) kind,
) path: await getCommandOutput(command)
}))
); );
const results = pathOutputs.map(item => { const cachePaths: CacheDirectory[] = [];
for (const item of pathOutputs) {
if (item.status === 'fulfilled') { if (item.status === 'fulfilled') {
return item.value; if (item.value.path) {
cachePaths.push(item.value);
}
} else { } else {
core.info(`[warning]getting cache directory path failed: ${item.reason}`); core.info(`[warning]getting cache directory path failed: ${item.reason}`);
} }
}
return '';
});
const cachePaths = results.filter(item => item);
if (!cachePaths.length) { if (!cachePaths.length) {
throw new Error(`Could not get cache folder paths.`); throw new Error(`Could not get cache folder paths.`);
@@ -59,6 +75,69 @@ export const getCacheDirectoryPath = async (
return cachePaths; return cachePaths;
}; };
export function canRestoreCachesInParallel(
directories: CacheDirectory[]
): boolean {
try {
const paths = directories.map(directory => {
const resolvedPath = resolveCachePath(directory.path);
// Conservatively handle case-insensitive Windows/macOS filesystems.
return process.platform === 'darwin' || process.platform === 'win32'
? resolvedPath.toLowerCase()
: resolvedPath;
});
// A cache archive can replace symlinks traversed by the other restore.
// Canonical destinations alone do not capture that extraction footprint.
if (
paths.some((resolvedPath, index) => {
const absolutePath = path.resolve(directories[index].path);
const normalizedPath =
process.platform === 'darwin' || process.platform === 'win32'
? absolutePath.toLowerCase()
: absolutePath;
return resolvedPath !== normalizedPath;
})
) {
return false;
}
return paths.every((parent, index) =>
paths.every((child, otherIndex) => {
if (index === otherIndex) {
return true;
}
const relative = path.relative(parent, child);
return (
relative === '..' ||
relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative)
);
})
);
} catch {
// If the paths cannot be resolved safely, avoid concurrent extraction.
return false;
}
}
function resolveCachePath(directory: string): string {
const absolutePath = path.resolve(directory);
try {
fs.lstatSync(absolutePath);
} catch (error) {
const parent = path.dirname(absolutePath);
if (
(error as NodeJS.ErrnoException).code !== 'ENOENT' ||
parent === absolutePath
) {
throw error;
}
// Cold cache directories may not exist yet. Resolve their existing
// ancestors so symlink aliases still participate in the overlap check.
return path.join(resolveCachePath(parent), path.basename(absolutePath));
}
return fs.realpathSync(absolutePath);
}
export function isGhes(): boolean { export function isGhes(): boolean {
const ghUrl = new URL( const ghUrl = new URL(
process.env['GITHUB_SERVER_URL'] || 'https://github.com' process.env['GITHUB_SERVER_URL'] || 'https://github.com'

View File

@@ -1,8 +1,22 @@
export enum State { export enum State {
CachePrimaryKey = 'CACHE_KEY', CacheEntries = 'CACHE_ENTRIES'
CacheMatchedKey = 'CACHE_RESULT'
} }
export enum Outputs { export enum Outputs {
CacheHit = 'cache-hit' CacheHit = 'cache-hit',
GoBinPath = 'go-bin-path'
} }
export const GO_ENV_OUTPUTS = [
'GOPATH',
'GOBIN',
'GOROOT',
'GOCACHE',
'GOMODCACHE',
'GOOS',
'GOARCH',
'GOTOOLDIR'
] as const;
export type GoEnvVar = (typeof GO_ENV_OUTPUTS)[number];
export type GoEnv = Partial<Record<GoEnvVar, string>>;

13
src/go-version-fetch.ts Normal file
View File

@@ -0,0 +1,13 @@
import * as httpm from '@actions/http-client';
import type {IGoVersion} from './installer.js';
export async function getVersionsDist(
dlUrl: string
): Promise<IGoVersion[] | null> {
// this returns versions descending so latest is first
const http: httpm.HttpClient = new httpm.HttpClient('setup-go', [], {
allowRedirects: true,
maxRedirects: 3
});
return (await http.getJson<IGoVersion[]>(dlUrl)).result;
}

View File

@@ -3,13 +3,14 @@ import * as core from '@actions/core';
import * as path from 'path'; import * as path from 'path';
import * as semver from 'semver'; import * as semver from 'semver';
import * as httpm from '@actions/http-client'; import * as httpm from '@actions/http-client';
import * as sys from './system'; import * as sys from './system.js';
import crypto from 'crypto'; import crypto from 'crypto';
import cp from 'child_process'; import cp from 'child_process';
import fs from 'fs'; import fs from 'fs';
import os from 'os'; import os from 'os';
import {StableReleaseAlias, isSelfHosted} from './utils'; import {StableReleaseAlias, isSelfHosted} from './utils.js';
import {Architecture} from './types'; import {Architecture} from './types.js';
import {getVersionsDist} from './go-version-fetch.js';
export const GOTOOLCHAIN_ENV_VAR = 'GOTOOLCHAIN'; export const GOTOOLCHAIN_ENV_VAR = 'GOTOOLCHAIN';
export const GOTOOLCHAIN_LOCAL_VAL = 'local'; export const GOTOOLCHAIN_LOCAL_VAL = 'local';
@@ -167,12 +168,14 @@ export async function getGo(
if (err instanceof tc.HTTPError && err.httpStatusCode === 404) { if (err instanceof tc.HTTPError && err.httpStatusCode === 404) {
throw new Error( throw new Error(
`The requested Go version ${versionSpec} is not available for platform ${osPlat}/${arch}. ` + `The requested Go version ${versionSpec} is not available for platform ${osPlat}/${arch}. ` +
`Download URL returned HTTP 404: ${downloadUrl}` `Download URL returned HTTP 404: ${downloadUrl}`,
{cause: err}
); );
} }
throw new Error( throw new Error(
`Failed to download Go ${versionSpec} for platform ${osPlat}/${arch} ` + `Failed to download Go ${versionSpec} for platform ${osPlat}/${arch} ` +
`from ${downloadUrl}: ${err}` `from ${downloadUrl}: ${err}`,
{cause: err}
); );
} }
} else { } else {
@@ -218,7 +221,9 @@ export async function getGo(
core.info('Install from dist'); core.info('Install from dist');
downloadPath = await installGoVersion(info, undefined, arch); downloadPath = await installGoVersion(info, undefined, arch);
} catch (err) { } catch (err) {
throw new Error(`Failed to download version ${versionSpec}: ${err}`); throw new Error(`Failed to download version ${versionSpec}: ${err}`, {
cause: err
});
} }
} }
} }
@@ -569,9 +574,7 @@ export async function findMatch(
let result: IGoVersion | undefined; let result: IGoVersion | undefined;
let match: IGoVersion | undefined; let match: IGoVersion | undefined;
const candidates: IGoVersion[] | null = await module.exports.getVersionsDist( const candidates: IGoVersion[] | null = await getVersionsDist(dlUrl);
dlUrl
);
if (!candidates) { if (!candidates) {
throw new Error(`golang download url did not return results`); throw new Error(`golang download url did not return results`);
} }
@@ -607,17 +610,6 @@ export async function findMatch(
return result; return result;
} }
export async function getVersionsDist(
dlUrl: string
): Promise<IGoVersion[] | null> {
// this returns versions descending so latest is first
const http: httpm.HttpClient = new httpm.HttpClient('setup-go', [], {
allowRedirects: true,
maxRedirects: 3
});
return (await http.getJson<IGoVersion[]>(dlUrl)).result;
}
// //
// Convert the go version syntax into semver for semver matching // Convert the go version syntax into semver for semver matching
// 1.13.1 => 1.13.1 // 1.13.1 => 1.13.1
@@ -686,9 +678,8 @@ async function resolveStableVersionDist(
) { ) {
const archFilter = sys.getArch(arch); const archFilter = sys.getArch(arch);
const platFilter = sys.getPlatform(); const platFilter = sys.getPlatform();
const candidates: IGoVersion[] | null = await module.exports.getVersionsDist( const candidates: IGoVersion[] | null =
GOLANG_DOWNLOAD_URL await getVersionsDist(GOLANG_DOWNLOAD_URL);
);
if (!candidates) { if (!candidates) {
throw new Error(`golang download url did not return results`); throw new Error(`golang download url did not return results`);
} }

View File

@@ -1,14 +1,16 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as io from '@actions/io'; import * as io from '@actions/io';
import * as installer from './installer'; import * as installer from './installer.js';
import * as semver from 'semver'; import * as semver from 'semver';
import path from 'path'; import path from 'path';
import {restoreCache} from './cache-restore'; import {fileURLToPath} from 'url';
import {isCacheFeatureAvailable} from './cache-utils'; import {restoreCache} from './cache-restore.js';
import {isCacheFeatureAvailable} from './cache-utils.js';
import cp from 'child_process'; import cp from 'child_process';
import fs from 'fs'; import fs from 'fs';
import os from 'os'; import os from 'os';
import {Architecture} from './types'; import {Architecture} from './types.js';
import {GO_ENV_OUTPUTS, GoEnv, Outputs} from './constants.js';
export async function run() { export async function run() {
try { try {
@@ -20,7 +22,6 @@ export async function run() {
setGoToolchain(); setGoToolchain();
const cache = core.getBooleanInput('cache'); const cache = core.getBooleanInput('cache');
core.info(`Setup go version spec ${versionSpec}`);
let arch = core.getInput('architecture') as Architecture; let arch = core.getInput('architecture') as Architecture;
@@ -29,6 +30,10 @@ export async function run() {
} }
if (versionSpec) { if (versionSpec) {
core.startGroup('Installed version');
try {
core.info(`Setup go version spec ${versionSpec}`);
const token = core.getInput('token'); const token = core.getInput('token');
const auth = !token ? undefined : `token ${token}`; const auth = !token ? undefined : `token ${token}`;
@@ -64,6 +69,9 @@ export async function run() {
} }
core.info(`Successfully set up Go version ${versionSpec}`); core.info(`Successfully set up Go version ${versionSpec}`);
} finally {
core.endGroup();
}
} else { } else {
core.info( core.info(
'[warning]go-version input was not specified. The action will try to use pre-installed version.' '[warning]go-version input was not specified. The action will try to use pre-installed version.'
@@ -75,6 +83,11 @@ export async function run() {
const goPath = await io.which('go'); const goPath = await io.which('go');
const goVersion = (cp.execSync(`${goPath} version`) || '').toString(); const goVersion = (cp.execSync(`${goPath} version`) || '').toString();
const goEnvJson = readGoEnv(goPath);
if (goEnvJson) {
setGoEnvOutputs(goEnvJson);
}
if (cache && isCacheFeatureAvailable()) { if (cache && isCacheFeatureAvailable()) {
const packageManager = 'default'; const packageManager = 'default';
@@ -91,7 +104,11 @@ export async function run() {
} }
// add problem matchers // add problem matchers
const matchersPath = path.join(__dirname, '../..', 'matchers.json'); const matchersPath = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'../..',
'matchers.json'
);
core.info(`##[add-matcher]${matchersPath}`); core.info(`##[add-matcher]${matchersPath}`);
// output the version actually being used // output the version actually being used
@@ -108,6 +125,46 @@ export async function run() {
} }
} }
export function readGoEnv(goPath: string): GoEnv | undefined {
try {
const rawGoEnv = cp.execFileSync(goPath, ['env', '-json'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
});
const parsed: unknown = JSON.parse(rawGoEnv);
if (
typeof parsed !== 'object' ||
parsed === null ||
Array.isArray(parsed)
) {
throw new Error("'go env -json' did not return a JSON object");
}
return parsed as GoEnv;
} catch (error) {
core.info(
`Unable to read 'go env -json', the Go environment outputs will not be set: ${
(error as Error).message
}`
);
return undefined;
}
}
export function setGoEnvOutputs(goEnv: GoEnv): void {
for (const name of GO_ENV_OUTPUTS) {
core.setOutput(name, goEnv[name] ?? '');
}
core.setOutput(Outputs.GoBinPath, goEnv.GOBIN || goPathBin(goEnv));
}
function goPathBin(goEnv: GoEnv): string {
const goPath = (goEnv.GOPATH ?? '').split(path.delimiter)[0];
return goPath ? path.join(goPath, 'bin') : '';
}
export async function addBinToPath(): Promise<boolean> { export async function addBinToPath(): Promise<boolean> {
let added = false; let added = false;
const g = await io.which('go'); const g = await io.which('go');

View File

@@ -2,14 +2,19 @@ type SupportedPackageManagers = {
[prop: string]: PackageManagerInfo; [prop: string]: PackageManagerInfo;
}; };
export type CacheKind = 'modules' | 'build';
export interface PackageManagerInfo { export interface PackageManagerInfo {
dependencyFilePattern: string; dependencyFilePattern: string;
cacheFolderCommandList: string[]; cacheFolderCommandList: {kind: CacheKind; command: string}[];
} }
export const supportedPackageManagers: SupportedPackageManagers = { export const supportedPackageManagers: SupportedPackageManagers = {
default: { default: {
dependencyFilePattern: 'go.mod', dependencyFilePattern: 'go.mod',
cacheFolderCommandList: ['go env GOMODCACHE', 'go env GOCACHE'] cacheFolderCommandList: [
{kind: 'modules', command: 'go env GOMODCACHE'},
{kind: 'build', command: 'go env GOCACHE'}
]
} }
}; };

View File

@@ -1,3 +1,3 @@
import {run} from './main'; import {run} from './main.js';
run(); run();

View File

@@ -1,5 +1,5 @@
import os from 'os'; import os from 'os';
import {Architecture} from './types'; import {Architecture} from './types.js';
export function getPlatform(): string { export function getPlatform(): string {
// darwin and linux match already // darwin and linux match already

View File

@@ -2,8 +2,8 @@
"compilerOptions": { "compilerOptions": {
/* Basic Options */ /* Basic Options */
// "incremental": true, /* Enable incremental compilation */ // "incremental": true, /* Enable incremental compilation */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "target": "ES2022", /* 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'. */ "module": "NodeNext", /* Specify module code generation. */
// "allowJs": true, /* Allow javascript files to be compiled. */ // "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */ // "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
@@ -38,7 +38,7 @@
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */ /* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "moduleResolution": "NodeNext", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "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. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
@@ -61,5 +61,5 @@
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}, },
"exclude": ["node_modules", "**/*.test.ts"] "exclude": ["node_modules", "**/*.test.ts", "jest.config.ts"]
} }