mirror of
https://github.com/actions/cache.git
synced 2026-06-30 09:08:15 +00:00
Switched to forked toolkit
This commit is contained in:
parent
8cf0abb534
commit
fed9ab945e
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@ -1,2 +1,2 @@
|
|||||||
* @justvanilla
|
* @justvanilla
|
||||||
* @inossidabile
|
* inossidabile
|
||||||
26
.github/workflows/release.yml
vendored
Normal file
26
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
name: Release new action version
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [released]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag_name:
|
||||||
|
description: 'Tag name that the major tag will point to'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
tag_name: ${{ github.event.inputs.tag_name || github.event.release.tag_name }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update_tag:
|
||||||
|
name: Release the new action
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Update the ${{ env.tag_name }} tag
|
||||||
|
uses: actions/publish-action@v0.2.2
|
||||||
|
with:
|
||||||
|
source-tag: ${{ env.tag_name }}
|
||||||
21
.github/workflows/sync-fork.yml
vendored
Normal file
21
.github/workflows/sync-fork.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
name: Sync Upstream
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '*/30 * * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions: write-all
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- uses: fopina/upstream-to-pr@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
upstream-repository: https://github.com/actions/cache.git
|
||||||
BIN
.licenses/npm/@actions/cache.dep.yml
generated
Normal file
BIN
.licenses/npm/@actions/cache.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@actions/glob-0.1.2.dep.yml
generated
Normal file
BIN
.licenses/npm/@actions/glob-0.1.2.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@azure/abort-controller.dep.yml
generated
Normal file
BIN
.licenses/npm/@azure/abort-controller.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@azure/core-auth.dep.yml
generated
Normal file
BIN
.licenses/npm/@azure/core-auth.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@azure/core-http.dep.yml
generated
Normal file
BIN
.licenses/npm/@azure/core-http.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@azure/core-lro.dep.yml
generated
Normal file
BIN
.licenses/npm/@azure/core-lro.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@azure/core-paging.dep.yml
generated
Normal file
BIN
.licenses/npm/@azure/core-paging.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@azure/core-tracing.dep.yml
generated
Normal file
BIN
.licenses/npm/@azure/core-tracing.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@azure/core-util.dep.yml
generated
Normal file
BIN
.licenses/npm/@azure/core-util.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@azure/logger.dep.yml
generated
Normal file
BIN
.licenses/npm/@azure/logger.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@azure/ms-rest-js.dep.yml
generated
Normal file
BIN
.licenses/npm/@azure/ms-rest-js.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@azure/storage-blob.dep.yml
generated
Normal file
BIN
.licenses/npm/@azure/storage-blob.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@opentelemetry/api.dep.yml
generated
Normal file
BIN
.licenses/npm/@opentelemetry/api.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@types/node-fetch.dep.yml
generated
Normal file
BIN
.licenses/npm/@types/node-fetch.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@types/node.dep.yml
generated
Normal file
BIN
.licenses/npm/@types/node.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@types/tunnel.dep.yml
generated
Normal file
BIN
.licenses/npm/@types/tunnel.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/abort-controller.dep.yml
generated
Normal file
BIN
.licenses/npm/abort-controller.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/asynckit.dep.yml
generated
Normal file
BIN
.licenses/npm/asynckit.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/combined-stream.dep.yml
generated
Normal file
BIN
.licenses/npm/combined-stream.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/delayed-stream.dep.yml
generated
Normal file
BIN
.licenses/npm/delayed-stream.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/event-target-shim.dep.yml
generated
Normal file
BIN
.licenses/npm/event-target-shim.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/form-data-2.5.1.dep.yml
generated
Normal file
BIN
.licenses/npm/form-data-2.5.1.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/form-data-3.0.1.dep.yml
generated
Normal file
BIN
.licenses/npm/form-data-3.0.1.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/form-data-4.0.0.dep.yml
generated
Normal file
BIN
.licenses/npm/form-data-4.0.0.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/github-actions.cache-s3.dep.yml
generated
Normal file
BIN
.licenses/npm/github-actions.cache-s3.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/ip-regex.dep.yml
generated
Normal file
BIN
.licenses/npm/ip-regex.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/mime-db.dep.yml
generated
Normal file
BIN
.licenses/npm/mime-db.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/mime-types.dep.yml
generated
Normal file
BIN
.licenses/npm/mime-types.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/node-fetch.dep.yml
generated
Normal file
BIN
.licenses/npm/node-fetch.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/process.dep.yml
generated
Normal file
BIN
.licenses/npm/process.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/psl.dep.yml
generated
Normal file
BIN
.licenses/npm/psl.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/punycode.dep.yml
generated
Normal file
BIN
.licenses/npm/punycode.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/sax.dep.yml
generated
Normal file
BIN
.licenses/npm/sax.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/semver-6.3.0.dep.yml
generated
Normal file
BIN
.licenses/npm/semver-6.3.0.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/tough-cookie.dep.yml
generated
Normal file
BIN
.licenses/npm/tough-cookie.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/tr46.dep.yml
generated
Normal file
BIN
.licenses/npm/tr46.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/uuid-3.4.0.dep.yml
generated
Normal file
BIN
.licenses/npm/uuid-3.4.0.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/webidl-conversions.dep.yml
generated
Normal file
BIN
.licenses/npm/webidl-conversions.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/whatwg-url.dep.yml
generated
Normal file
BIN
.licenses/npm/whatwg-url.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/xml2js.dep.yml
generated
Normal file
BIN
.licenses/npm/xml2js.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/xmlbuilder.dep.yml
generated
Normal file
BIN
.licenses/npm/xmlbuilder.dep.yml
generated
Normal file
Binary file not shown.
@ -1,12 +1,12 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
import * as cache from "github-actions.cache-s3";
|
||||||
|
|
||||||
import * as cache from "../src/backend";
|
|
||||||
import { Events, RefKey } from "../src/constants";
|
import { Events, RefKey } from "../src/constants";
|
||||||
import * as actionUtils from "../src/utils/actionUtils";
|
import * as actionUtils from "../src/utils/actionUtils";
|
||||||
import * as testUtils from "../src/utils/testUtils";
|
import * as testUtils from "../src/utils/testUtils";
|
||||||
|
|
||||||
jest.mock("@actions/core");
|
jest.mock("@actions/core");
|
||||||
jest.mock("../src/backend");
|
jest.mock("github-actions.cache-s3");
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
|
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
import * as cache from "github-actions.cache-s3";
|
||||||
|
|
||||||
import * as cache from "../src/backend";
|
|
||||||
import { Events, RefKey } from "../src/constants";
|
import { Events, RefKey } from "../src/constants";
|
||||||
import run from "../src/restore";
|
import run from "../src/restore";
|
||||||
import * as actionUtils from "../src/utils/actionUtils";
|
import * as actionUtils from "../src/utils/actionUtils";
|
||||||
@ -81,7 +81,11 @@ test("restore with no cache found", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -125,7 +129,11 @@ test("restore with restore keys and no cache found", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -168,7 +176,11 @@ test("restore with cache found for key", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -214,7 +226,11 @@ test("restore with cache found for restore key", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -260,7 +276,11 @@ test("Fail restore when fail on cache miss is enabled and primary + restore keys
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -304,7 +324,11 @@ test("restore when fail on cache miss is enabled and primary key doesn't match r
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -351,7 +375,11 @@ test("restore with fail on cache miss disabled and no cache found", async () =>
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
import * as cache from "github-actions.cache-s3";
|
||||||
|
|
||||||
import * as cache from "../src/backend";
|
|
||||||
import { Events, Inputs, RefKey } from "../src/constants";
|
import { Events, Inputs, RefKey } from "../src/constants";
|
||||||
import run from "../src/restoreImpl";
|
import run from "../src/restoreImpl";
|
||||||
import { StateProvider } from "../src/stateProvider";
|
import { StateProvider } from "../src/stateProvider";
|
||||||
@ -129,7 +129,11 @@ test("restore on GHES with AC available ", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -184,7 +188,11 @@ test("restore with too many keys should fail", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
expect(failedMock).toHaveBeenCalledWith(
|
expect(failedMock).toHaveBeenCalledWith(
|
||||||
@ -211,7 +219,11 @@ test("restore with large key should fail", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
expect(failedMock).toHaveBeenCalledWith(
|
expect(failedMock).toHaveBeenCalledWith(
|
||||||
@ -238,7 +250,11 @@ test("restore with invalid key should fail", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
expect(failedMock).toHaveBeenCalledWith(
|
expect(failedMock).toHaveBeenCalledWith(
|
||||||
@ -274,7 +290,11 @@ test("restore with no cache found", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -316,7 +336,11 @@ test("restore with restore keys and no cache found", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -357,7 +381,11 @@ test("restore with cache found for key", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -400,7 +428,11 @@ test("restore with cache found for restore key", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -442,7 +474,11 @@ test("restore with lookup-only set", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: true
|
lookupOnly: true
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
import * as cache from "github-actions.cache-s3";
|
||||||
|
|
||||||
import * as cache from "../src/backend";
|
|
||||||
import { Events, RefKey } from "../src/constants";
|
import { Events, RefKey } from "../src/constants";
|
||||||
import run from "../src/restoreOnly";
|
import run from "../src/restoreOnly";
|
||||||
import * as actionUtils from "../src/utils/actionUtils";
|
import * as actionUtils from "../src/utils/actionUtils";
|
||||||
@ -82,7 +82,11 @@ test("restore with no cache found", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -125,7 +129,11 @@ test("restore with restore keys and no cache found", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -165,7 +173,11 @@ test("restore with cache found for key", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -209,7 +221,11 @@ test("restore with cache found for restore key", async () => {
|
|||||||
{
|
{
|
||||||
lookupOnly: false
|
lookupOnly: false
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
import * as cache from "github-actions.cache-s3";
|
||||||
|
|
||||||
import * as cache from "../src/backend";
|
|
||||||
import { Events, Inputs, RefKey } from "../src/constants";
|
import { Events, Inputs, RefKey } from "../src/constants";
|
||||||
import run from "../src/save";
|
import run from "../src/save";
|
||||||
import * as actionUtils from "../src/utils/actionUtils";
|
import * as actionUtils from "../src/utils/actionUtils";
|
||||||
import * as testUtils from "../src/utils/testUtils";
|
import * as testUtils from "../src/utils/testUtils";
|
||||||
|
|
||||||
jest.mock("@actions/core");
|
jest.mock("@actions/core");
|
||||||
jest.mock("../src/backend");
|
jest.mock("github-actions.cache-s3");
|
||||||
jest.mock("../src/utils/actionUtils");
|
jest.mock("../src/utils/actionUtils");
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@ -109,7 +109,11 @@ test("save with valid inputs uploads a cache", async () => {
|
|||||||
{
|
{
|
||||||
uploadChunkSize: 4000000
|
uploadChunkSize: 4000000
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
import * as cache from "github-actions.cache-s3";
|
||||||
|
|
||||||
import * as cache from "../src/backend";
|
|
||||||
import { Events, Inputs, RefKey } from "../src/constants";
|
import { Events, Inputs, RefKey } from "../src/constants";
|
||||||
import run from "../src/saveImpl";
|
import run from "../src/saveImpl";
|
||||||
import { StateProvider } from "../src/stateProvider";
|
import { StateProvider } from "../src/stateProvider";
|
||||||
@ -8,7 +8,7 @@ import * as actionUtils from "../src/utils/actionUtils";
|
|||||||
import * as testUtils from "../src/utils/testUtils";
|
import * as testUtils from "../src/utils/testUtils";
|
||||||
|
|
||||||
jest.mock("@actions/core");
|
jest.mock("@actions/core");
|
||||||
jest.mock("../src/backend");
|
jest.mock("github-actions.cache-s3");
|
||||||
jest.mock("../src/utils/actionUtils");
|
jest.mock("../src/utils/actionUtils");
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@ -170,7 +170,11 @@ test("save on GHES with AC available", async () => {
|
|||||||
{
|
{
|
||||||
uploadChunkSize: 4000000
|
uploadChunkSize: 4000000
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -267,7 +271,11 @@ test("save with large cache outputs warning", async () => {
|
|||||||
[inputPath],
|
[inputPath],
|
||||||
primaryKey,
|
primaryKey,
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -301,7 +309,7 @@ test("save with reserve cache failure outputs warning", async () => {
|
|||||||
const saveCacheMock = jest
|
const saveCacheMock = jest
|
||||||
.spyOn(cache, "saveCache")
|
.spyOn(cache, "saveCache")
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
const actualCache = jest.requireActual("../src/backend");
|
const actualCache = jest.requireActual("github-actions.cache-s3");
|
||||||
const error = new actualCache.ReserveCacheError(
|
const error = new actualCache.ReserveCacheError(
|
||||||
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
|
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
|
||||||
);
|
);
|
||||||
@ -315,7 +323,11 @@ test("save with reserve cache failure outputs warning", async () => {
|
|||||||
[inputPath],
|
[inputPath],
|
||||||
primaryKey,
|
primaryKey,
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -359,7 +371,11 @@ test("save with server error outputs warning", async () => {
|
|||||||
[inputPath],
|
[inputPath],
|
||||||
primaryKey,
|
primaryKey,
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -405,7 +421,11 @@ test("save with valid inputs uploads a cache", async () => {
|
|||||||
{
|
{
|
||||||
uploadChunkSize: 4000000
|
uploadChunkSize: 4000000
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
import * as cache from "github-actions.cache-s3";
|
||||||
|
|
||||||
import * as cache from "../src/backend";
|
|
||||||
import { Events, Inputs, RefKey } from "../src/constants";
|
import { Events, Inputs, RefKey } from "../src/constants";
|
||||||
import run from "../src/saveOnly";
|
import run from "../src/saveOnly";
|
||||||
import * as actionUtils from "../src/utils/actionUtils";
|
import * as actionUtils from "../src/utils/actionUtils";
|
||||||
import * as testUtils from "../src/utils/testUtils";
|
import * as testUtils from "../src/utils/testUtils";
|
||||||
|
|
||||||
jest.mock("@actions/core");
|
jest.mock("@actions/core");
|
||||||
jest.mock("../src/backend");
|
jest.mock("github-actions.cache-s3");
|
||||||
jest.mock("../src/utils/actionUtils");
|
jest.mock("../src/utils/actionUtils");
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@ -99,7 +99,11 @@ test("save with valid inputs uploads a cache", async () => {
|
|||||||
{
|
{
|
||||||
uploadChunkSize: 4000000
|
uploadChunkSize: 4000000
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -132,7 +136,11 @@ test("save failing logs the warning message", async () => {
|
|||||||
{
|
{
|
||||||
uploadChunkSize: 4000000
|
uploadChunkSize: 4000000
|
||||||
},
|
},
|
||||||
{ credentials: { accessKeyId: "", secretAccessKey: "" }, region: "" },
|
{
|
||||||
|
credentials: { accessKeyId: "", secretAccessKey: "" },
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: ""
|
||||||
|
},
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
60507
dist/restore-only/index.js
vendored
60507
dist/restore-only/index.js
vendored
File diff suppressed because one or more lines are too long
60507
dist/restore/index.js
vendored
60507
dist/restore/index.js
vendored
File diff suppressed because one or more lines are too long
60507
dist/save-only/index.js
vendored
60507
dist/save-only/index.js
vendored
File diff suppressed because one or more lines are too long
60507
dist/save/index.js
vendored
60507
dist/save/index.js
vendored
File diff suppressed because one or more lines are too long
964
package-lock.json
generated
964
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cache",
|
"name": "cache",
|
||||||
"version": "3.3.1",
|
"version": "3.3.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Cache dependencies and build outputs",
|
"description": "Cache dependencies and build outputs",
|
||||||
"main": "dist/restore/index.js",
|
"main": "dist/restore/index.js",
|
||||||
@ -9,11 +9,12 @@
|
|||||||
"test": "tsc --noEmit && jest --coverage",
|
"test": "tsc --noEmit && jest --coverage",
|
||||||
"lint": "eslint **/*.ts --cache --fix",
|
"lint": "eslint **/*.ts --cache --fix",
|
||||||
"format": "prettier --write **/*.ts",
|
"format": "prettier --write **/*.ts",
|
||||||
"format-check": "prettier --check **/*.ts"
|
"format-check": "prettier --check **/*.ts",
|
||||||
|
"bump:patch": "npm version patch"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/actions/cache.git"
|
"url": "git+https://github.com/justvanilla/shared-gha-cache-s3.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"actions",
|
"actions",
|
||||||
@ -23,6 +24,7 @@
|
|||||||
"author": "GitHub",
|
"author": "GitHub",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"github-actions.cache-s3": "^1.0.1",
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"@actions/glob": "^0.4.0",
|
"@actions/glob": "^0.4.0",
|
||||||
|
|||||||
253
src/backend.ts
253
src/backend.ts
@ -1,253 +0,0 @@
|
|||||||
import * as core from "@actions/core";
|
|
||||||
import { S3ClientConfig } from "@aws-sdk/client-s3";
|
|
||||||
import * as path from "path";
|
|
||||||
|
|
||||||
import * as utils from "./utils/cacheUtils";
|
|
||||||
import * as cacheHttpClient from "./utils/clientUtils";
|
|
||||||
import { DownloadOptions, UploadOptions } from "./utils/contracts";
|
|
||||||
import { createTar, extractTar, listTar } from "./utils/tar";
|
|
||||||
|
|
||||||
export class ValidationError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
this.name = "ValidationError";
|
|
||||||
Object.setPrototypeOf(this, ValidationError.prototype);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ReserveCacheError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
this.name = "ReserveCacheError";
|
|
||||||
Object.setPrototypeOf(this, ReserveCacheError.prototype);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkPaths(paths: string[]): void {
|
|
||||||
if (!paths || paths.length === 0) {
|
|
||||||
throw new ValidationError(
|
|
||||||
`Path Validation Error: At least one directory or file path is required`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkKey(key: string): void {
|
|
||||||
if (key.length > 512) {
|
|
||||||
throw new ValidationError(
|
|
||||||
`Key Validation Error: ${key} cannot be larger than 512 characters.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const regex = /^[^,]*$/;
|
|
||||||
if (!regex.test(key)) {
|
|
||||||
throw new ValidationError(
|
|
||||||
`Key Validation Error: ${key} cannot contain commas.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* isFeatureAvailable to check the presence of Actions cache service
|
|
||||||
*
|
|
||||||
* @returns boolean return true if Actions cache service feature is available, otherwise false
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function isFeatureAvailable(): boolean {
|
|
||||||
return !!process.env["ACTIONS_CACHE_URL"];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores cache from keys
|
|
||||||
*
|
|
||||||
* @param paths a list of file paths to restore from the cache
|
|
||||||
* @param primaryKey an explicit key for restoring the cache
|
|
||||||
* @param restoreKeys an optional ordered list of keys to use for restoring the cache if no cache hit occurred for key
|
|
||||||
* @param options cache download options
|
|
||||||
* @param s3Options upload options for AWS S3
|
|
||||||
* @param s3BucketName a name of AWS S3 bucket
|
|
||||||
* @returns string returns the key for the cache hit, otherwise returns undefined
|
|
||||||
*/
|
|
||||||
export async function restoreCache(
|
|
||||||
paths: string[],
|
|
||||||
primaryKey: string,
|
|
||||||
restoreKeys?: string[],
|
|
||||||
options?: DownloadOptions,
|
|
||||||
s3Options?: S3ClientConfig,
|
|
||||||
s3BucketName?: string
|
|
||||||
): Promise<string | undefined> {
|
|
||||||
checkPaths(paths);
|
|
||||||
|
|
||||||
restoreKeys = restoreKeys || [];
|
|
||||||
const keys = [primaryKey, ...restoreKeys];
|
|
||||||
|
|
||||||
core.debug("Resolved Keys:");
|
|
||||||
core.debug(JSON.stringify(keys));
|
|
||||||
|
|
||||||
if (keys.length > 10) {
|
|
||||||
throw new ValidationError(
|
|
||||||
`Key Validation Error: Keys are limited to a maximum of 10.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (const key of keys) {
|
|
||||||
checkKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
const compressionMethod = await utils.getCompressionMethod();
|
|
||||||
|
|
||||||
// path are needed to compute version
|
|
||||||
const cacheEntry = await cacheHttpClient.getCacheEntry(
|
|
||||||
keys,
|
|
||||||
paths,
|
|
||||||
{
|
|
||||||
compressionMethod
|
|
||||||
},
|
|
||||||
s3Options,
|
|
||||||
s3BucketName
|
|
||||||
);
|
|
||||||
if (!cacheEntry || (!cacheEntry.archiveLocation && !cacheEntry.cacheKey)) {
|
|
||||||
// Cache not found
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const archivePath = path.join(
|
|
||||||
await utils.createTempDirectory(),
|
|
||||||
utils.getCacheFileName(compressionMethod)
|
|
||||||
);
|
|
||||||
core.debug(`Archive Path: ${archivePath}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Download the cache from the cache entry
|
|
||||||
await cacheHttpClient.downloadCache(
|
|
||||||
cacheEntry,
|
|
||||||
archivePath,
|
|
||||||
options,
|
|
||||||
s3Options,
|
|
||||||
s3BucketName
|
|
||||||
);
|
|
||||||
|
|
||||||
if (core.isDebug()) {
|
|
||||||
await listTar(archivePath, compressionMethod);
|
|
||||||
}
|
|
||||||
|
|
||||||
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath);
|
|
||||||
core.info(
|
|
||||||
`Cache Size: ~${Math.round(
|
|
||||||
archiveFileSize / (1024 * 1024)
|
|
||||||
)} MB (${archiveFileSize} B)`
|
|
||||||
);
|
|
||||||
|
|
||||||
await extractTar(archivePath, compressionMethod);
|
|
||||||
core.info("Cache restored successfully");
|
|
||||||
} finally {
|
|
||||||
// Try to delete the archive to save space
|
|
||||||
try {
|
|
||||||
await utils.unlinkFile(archivePath);
|
|
||||||
} catch (error) {
|
|
||||||
core.debug(`Failed to delete archive: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cacheEntry.cacheKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves a list of files with the specified key
|
|
||||||
*
|
|
||||||
* @param paths a list of file paths to be cached
|
|
||||||
* @param key an explicit key for restoring the cache
|
|
||||||
* @param options cache upload options
|
|
||||||
* @param s3Options upload options for AWS S3
|
|
||||||
* @param s3BucketName a name of AWS S3 bucket
|
|
||||||
* @returns number returns cacheId if the cache was saved successfully and throws an error if save fails
|
|
||||||
*/
|
|
||||||
export async function saveCache(
|
|
||||||
paths: string[],
|
|
||||||
key: string,
|
|
||||||
options?: UploadOptions,
|
|
||||||
s3Options?: S3ClientConfig,
|
|
||||||
s3BucketName?: string
|
|
||||||
): Promise<number> {
|
|
||||||
checkPaths(paths);
|
|
||||||
checkKey(key);
|
|
||||||
|
|
||||||
const compressionMethod = await utils.getCompressionMethod();
|
|
||||||
let cacheId = 0;
|
|
||||||
|
|
||||||
const cachePaths = await utils.resolvePaths(paths);
|
|
||||||
core.debug("Cache Paths:");
|
|
||||||
core.debug(`${JSON.stringify(cachePaths)}`);
|
|
||||||
|
|
||||||
const archiveFolder = await utils.createTempDirectory();
|
|
||||||
const archivePath = path.join(
|
|
||||||
archiveFolder,
|
|
||||||
utils.getCacheFileName(compressionMethod)
|
|
||||||
);
|
|
||||||
|
|
||||||
core.debug(`Archive Path: ${archivePath}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await createTar(archiveFolder, cachePaths, compressionMethod);
|
|
||||||
if (core.isDebug()) {
|
|
||||||
await listTar(archivePath, compressionMethod);
|
|
||||||
}
|
|
||||||
const fileSizeLimit = 10 * 1024 * 1024 * 1024; // 10GB per repo limit
|
|
||||||
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath);
|
|
||||||
core.debug(`File Size: ${archiveFileSize}`);
|
|
||||||
|
|
||||||
// For GHES, this check will take place in ReserveCache API with enterprise file size limit
|
|
||||||
if (archiveFileSize > fileSizeLimit) {
|
|
||||||
throw new Error(
|
|
||||||
`Cache size of ~${Math.round(
|
|
||||||
archiveFileSize / (1024 * 1024)
|
|
||||||
)} MB (${archiveFileSize} B) is over the 10GB limit, not saving cache.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(s3Options && s3BucketName)) {
|
|
||||||
core.debug("Reserving Cache");
|
|
||||||
const reserveCacheResponse = await cacheHttpClient.reserveCache(
|
|
||||||
key,
|
|
||||||
paths,
|
|
||||||
{
|
|
||||||
compressionMethod,
|
|
||||||
cacheSize: archiveFileSize
|
|
||||||
},
|
|
||||||
s3Options,
|
|
||||||
s3BucketName
|
|
||||||
);
|
|
||||||
|
|
||||||
if (reserveCacheResponse?.result?.cacheId) {
|
|
||||||
cacheId = reserveCacheResponse?.result?.cacheId;
|
|
||||||
} else if (reserveCacheResponse?.statusCode === 400) {
|
|
||||||
throw new Error(
|
|
||||||
reserveCacheResponse?.error?.message ??
|
|
||||||
`Cache size of ~${Math.round(
|
|
||||||
archiveFileSize / (1024 * 1024)
|
|
||||||
)} MB (${archiveFileSize} B) is over the data cap limit, not saving cache.`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new ReserveCacheError(
|
|
||||||
`Unable to reserve cache with key ${key}, another job may be creating this cache. More details: ${reserveCacheResponse?.error?.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
core.debug(`Saving Cache (ID: ${cacheId})`);
|
|
||||||
await cacheHttpClient.saveCache(
|
|
||||||
cacheId,
|
|
||||||
archivePath,
|
|
||||||
key,
|
|
||||||
options,
|
|
||||||
s3Options,
|
|
||||||
s3BucketName
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
// Try to delete the archive to save space
|
|
||||||
try {
|
|
||||||
await utils.unlinkFile(archivePath);
|
|
||||||
} catch (error) {
|
|
||||||
core.debug(`Failed to delete archive: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cacheId;
|
|
||||||
}
|
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
import * as cache from "github-actions.cache-s3";
|
||||||
|
import { getConfig } from "github-actions.cache-s3/lib/internal/config";
|
||||||
|
|
||||||
import * as cache from "./backend";
|
|
||||||
import { Events, Inputs, Outputs, State } from "./constants";
|
import { Events, Inputs, Outputs, State } from "./constants";
|
||||||
import { IStateProvider } from "./stateProvider";
|
import { IStateProvider } from "./stateProvider";
|
||||||
import * as utils from "./utils/actionUtils";
|
import * as utils from "./utils/actionUtils";
|
||||||
import { getConfig } from "./utils/options";
|
|
||||||
|
|
||||||
async function restoreImpl(
|
async function restoreImpl(
|
||||||
stateProvider: IStateProvider
|
stateProvider: IStateProvider
|
||||||
@ -35,7 +35,11 @@ async function restoreImpl(
|
|||||||
const failOnCacheMiss = utils.getInputAsBool(Inputs.FailOnCacheMiss);
|
const failOnCacheMiss = utils.getInputAsBool(Inputs.FailOnCacheMiss);
|
||||||
const lookupOnly = utils.getInputAsBool(Inputs.LookupOnly);
|
const lookupOnly = utils.getInputAsBool(Inputs.LookupOnly);
|
||||||
const s3Bucket = core.getInput(Inputs.AwsBucket);
|
const s3Bucket = core.getInput(Inputs.AwsBucket);
|
||||||
const s3Config = getConfig();
|
const s3Config = getConfig(
|
||||||
|
core.getInput(Inputs.AwsRegion),
|
||||||
|
core.getInput(Inputs.AwsAccessKeyId),
|
||||||
|
core.getInput(Inputs.AwsSecretAccessKey)
|
||||||
|
);
|
||||||
|
|
||||||
const cacheKey = await cache.restoreCache(
|
const cacheKey = await cache.restoreCache(
|
||||||
cachePaths,
|
cachePaths,
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
import * as cache from "github-actions.cache-s3";
|
||||||
|
import { getConfig } from "github-actions.cache-s3/lib/internal/config";
|
||||||
|
|
||||||
import * as cache from "./backend";
|
|
||||||
import { Events, Inputs, State } from "./constants";
|
import { Events, Inputs, State } from "./constants";
|
||||||
import { IStateProvider } from "./stateProvider";
|
import { IStateProvider } from "./stateProvider";
|
||||||
import * as utils from "./utils/actionUtils";
|
import * as utils from "./utils/actionUtils";
|
||||||
import { getConfig } from "./utils/options";
|
|
||||||
|
|
||||||
// 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
|
||||||
@ -53,7 +53,11 @@ async function saveImpl(stateProvider: IStateProvider): Promise<number | void> {
|
|||||||
required: true
|
required: true
|
||||||
});
|
});
|
||||||
const s3Bucket = core.getInput(Inputs.AwsBucket);
|
const s3Bucket = core.getInput(Inputs.AwsBucket);
|
||||||
const s3Config = getConfig();
|
const s3Config = getConfig(
|
||||||
|
core.getInput(Inputs.AwsRegion),
|
||||||
|
core.getInput(Inputs.AwsAccessKeyId),
|
||||||
|
core.getInput(Inputs.AwsSecretAccessKey)
|
||||||
|
);
|
||||||
|
|
||||||
cacheId = await cache.saveCache(
|
cacheId = await cache.saveCache(
|
||||||
cachePaths,
|
cachePaths,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
import * as cache from "github-actions.cache-s3";
|
||||||
|
|
||||||
import * as cache from "../backend";
|
|
||||||
import { RefKey } from "../constants";
|
import { RefKey } from "../constants";
|
||||||
|
|
||||||
export function isGhes(): boolean {
|
export function isGhes(): boolean {
|
||||||
|
|||||||
@ -1,154 +0,0 @@
|
|||||||
import * as core from "@actions/core";
|
|
||||||
import * as exec from "@actions/exec";
|
|
||||||
import * as glob from "@actions/glob";
|
|
||||||
import * as io from "@actions/io";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
import * as semver from "semver";
|
|
||||||
import { v4 as uuidV4 } from "uuid";
|
|
||||||
|
|
||||||
export enum CacheFilename {
|
|
||||||
Gzip = "cache.tgz",
|
|
||||||
Zstd = "cache.tzst"
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum CompressionMethod {
|
|
||||||
Gzip = "gzip",
|
|
||||||
// Long range mode was added to zstd in v1.3.2.
|
|
||||||
// This enum is for earlier version of zstd that does not have --long support
|
|
||||||
ZstdWithoutLong = "zstd-without-long",
|
|
||||||
Zstd = "zstd"
|
|
||||||
}
|
|
||||||
|
|
||||||
// The default number of retry attempts.
|
|
||||||
export const DefaultRetryAttempts = 2;
|
|
||||||
|
|
||||||
// The default delay in milliseconds between retry attempts.
|
|
||||||
export const DefaultRetryDelay = 5000;
|
|
||||||
|
|
||||||
// Socket timeout in milliseconds during download. If no traffic is received
|
|
||||||
// over the socket during this period, the socket is destroyed and the download
|
|
||||||
// is aborted.
|
|
||||||
export const SocketTimeout = 5000;
|
|
||||||
|
|
||||||
async function getVersion(app: string): Promise<string> {
|
|
||||||
core.debug(`Checking ${app} --version`);
|
|
||||||
let versionOutput = "";
|
|
||||||
try {
|
|
||||||
await exec.exec(`${app} --version`, [], {
|
|
||||||
ignoreReturnCode: true,
|
|
||||||
silent: true,
|
|
||||||
listeners: {
|
|
||||||
stdout: (data: Buffer): string =>
|
|
||||||
(versionOutput += data.toString()),
|
|
||||||
stderr: (data: Buffer): string =>
|
|
||||||
(versionOutput += data.toString())
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err: any) {
|
|
||||||
core.debug(err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
versionOutput = versionOutput.trim();
|
|
||||||
core.debug(versionOutput);
|
|
||||||
return versionOutput;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getArchiveFileSizeInBytes(filePath: string): number {
|
|
||||||
return fs.statSync(filePath).size;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCacheFileName(compressionMethod: CompressionMethod): string {
|
|
||||||
return compressionMethod === CompressionMethod.Gzip
|
|
||||||
? CacheFilename.Gzip
|
|
||||||
: CacheFilename.Zstd;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createTempDirectory(): Promise<string> {
|
|
||||||
const IS_WINDOWS = process.platform === "win32";
|
|
||||||
|
|
||||||
let tempDirectory: string = process.env["RUNNER_TEMP"] || "";
|
|
||||||
|
|
||||||
if (!tempDirectory) {
|
|
||||||
let baseLocation: string;
|
|
||||||
if (IS_WINDOWS) {
|
|
||||||
// On Windows use the USERPROFILE env variable
|
|
||||||
baseLocation = process.env["USERPROFILE"] || "C:\\";
|
|
||||||
} else {
|
|
||||||
if (process.platform === "darwin") {
|
|
||||||
baseLocation = "/Users";
|
|
||||||
} else {
|
|
||||||
baseLocation = "/home";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tempDirectory = path.join(baseLocation, "actions", "temp");
|
|
||||||
}
|
|
||||||
|
|
||||||
const dest = path.join(tempDirectory, uuidV4());
|
|
||||||
await io.mkdirP(dest);
|
|
||||||
return dest;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resolvePaths(patterns: string[]): Promise<string[]> {
|
|
||||||
const paths: string[] = [];
|
|
||||||
const workspace = process.env["GITHUB_WORKSPACE"] ?? process.cwd();
|
|
||||||
const globber = await glob.create(patterns.join("\n"), {
|
|
||||||
implicitDescendants: false
|
|
||||||
});
|
|
||||||
|
|
||||||
for await (const file of globber.globGenerator()) {
|
|
||||||
const relativeFile = path
|
|
||||||
.relative(workspace, file)
|
|
||||||
.replace(new RegExp(`\\${path.sep}`, "g"), "/");
|
|
||||||
core.debug(`Matched: ${relativeFile}`);
|
|
||||||
// Paths are made relative so the tar entries are all relative to the root of the workspace.
|
|
||||||
paths.push(`${relativeFile}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return paths;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function isZstdInstalled(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await io.which("zstd", true);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function isGnuTarInstalled(): Promise<boolean> {
|
|
||||||
const versionOutput = await getVersion("tar");
|
|
||||||
return versionOutput.toLowerCase().includes("gnu tar");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCompressionMethod(): Promise<CompressionMethod> {
|
|
||||||
if (
|
|
||||||
(process.platform === "win32" && !(await isGnuTarInstalled())) ||
|
|
||||||
!(await isZstdInstalled())
|
|
||||||
) {
|
|
||||||
// Disable zstd due to bug https://github.com/actions/cache/issues/301
|
|
||||||
return CompressionMethod.Gzip;
|
|
||||||
}
|
|
||||||
|
|
||||||
const versionOutput = await getVersion("zstd");
|
|
||||||
const version = semver.clean(versionOutput);
|
|
||||||
|
|
||||||
// zstd is installed but using a version earlier than v1.3.2
|
|
||||||
// v1.3.2 is required to use the `--long` options in zstd
|
|
||||||
return !version || semver.lt(version, "v1.3.2")
|
|
||||||
? CompressionMethod.ZstdWithoutLong
|
|
||||||
: CompressionMethod.Zstd;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function unlinkFile(filePath: fs.PathLike): Promise<void> {
|
|
||||||
return await fs.promises.unlink(filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function assertDefined<T>(name: string, value?: T): T {
|
|
||||||
if (value === undefined) {
|
|
||||||
throw Error(`Expected ${name} but value was undefiend`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
@ -1,557 +0,0 @@
|
|||||||
import * as core from "@actions/core";
|
|
||||||
import { HttpClient } from "@actions/http-client";
|
|
||||||
import { BearerCredentialHandler } from "@actions/http-client/lib/auth";
|
|
||||||
import {
|
|
||||||
RequestOptions,
|
|
||||||
TypedResponse
|
|
||||||
} from "@actions/http-client/lib/interfaces";
|
|
||||||
import {
|
|
||||||
_Object,
|
|
||||||
ListObjectsV2Command,
|
|
||||||
ListObjectsV2CommandInput,
|
|
||||||
ListObjectsV2CommandOutput,
|
|
||||||
S3Client,
|
|
||||||
S3ClientConfig
|
|
||||||
} from "@aws-sdk/client-s3";
|
|
||||||
import { Progress, Upload } from "@aws-sdk/lib-storage";
|
|
||||||
import * as crypto from "crypto";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import { URL } from "url";
|
|
||||||
|
|
||||||
import * as utils from "./cacheUtils";
|
|
||||||
import { CompressionMethod } from "./cacheUtils";
|
|
||||||
import {
|
|
||||||
ArtifactCacheEntry,
|
|
||||||
CommitCacheRequest,
|
|
||||||
DownloadOptions,
|
|
||||||
InternalCacheOptions,
|
|
||||||
ITypedResponseWithError,
|
|
||||||
ReserveCacheRequest,
|
|
||||||
ReserveCacheResponse,
|
|
||||||
UploadOptions
|
|
||||||
} from "./contracts";
|
|
||||||
import {
|
|
||||||
downloadCacheHttpClient,
|
|
||||||
downloadCacheStorageS3
|
|
||||||
} from "./downloadUtils";
|
|
||||||
import { getDownloadOptions, getUploadOptions } from "./options";
|
|
||||||
import {
|
|
||||||
isSuccessStatusCode,
|
|
||||||
retryHttpClientResponse,
|
|
||||||
retryTypedResponse
|
|
||||||
} from "./requestUtils";
|
|
||||||
|
|
||||||
const versionSalt = "1.0";
|
|
||||||
|
|
||||||
function getCacheApiUrl(resource: string): string {
|
|
||||||
const baseUrl: string = process.env["ACTIONS_CACHE_URL"] || "";
|
|
||||||
if (!baseUrl) {
|
|
||||||
throw new Error(
|
|
||||||
"Cache Service Url not found, unable to restore cache."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `${baseUrl}_apis/artifactcache/${resource}`;
|
|
||||||
core.debug(`Resource Url: ${url}`);
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createAcceptHeader(type: string, apiVersion: string): string {
|
|
||||||
return `${type};api-version=${apiVersion}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRequestOptions(): RequestOptions {
|
|
||||||
const requestOptions: RequestOptions = {
|
|
||||||
headers: {
|
|
||||||
Accept: createAcceptHeader("application/json", "6.0-preview.1")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return requestOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createHttpClient(): HttpClient {
|
|
||||||
const token = process.env["ACTIONS_RUNTIME_TOKEN"] || "";
|
|
||||||
const bearerCredentialHandler = new BearerCredentialHandler(token);
|
|
||||||
|
|
||||||
return new HttpClient(
|
|
||||||
"actions/cache",
|
|
||||||
[bearerCredentialHandler],
|
|
||||||
getRequestOptions()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCacheVersion(
|
|
||||||
paths: string[],
|
|
||||||
compressionMethod?: CompressionMethod
|
|
||||||
): string {
|
|
||||||
const components = paths.concat(
|
|
||||||
!compressionMethod || compressionMethod === CompressionMethod.Gzip
|
|
||||||
? []
|
|
||||||
: [compressionMethod]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add salt to cache version to support breaking changes in cache entry
|
|
||||||
components.push(versionSalt);
|
|
||||||
|
|
||||||
return crypto
|
|
||||||
.createHash("sha256")
|
|
||||||
.update(components.join("|"))
|
|
||||||
.digest("hex");
|
|
||||||
}
|
|
||||||
|
|
||||||
interface _content {
|
|
||||||
Key?: string;
|
|
||||||
LastModified?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCacheEntryS3(
|
|
||||||
s3Options: S3ClientConfig,
|
|
||||||
s3BucketName: string,
|
|
||||||
keys: string[],
|
|
||||||
paths: string[]
|
|
||||||
): Promise<ArtifactCacheEntry | null> {
|
|
||||||
const primaryKey = keys[0];
|
|
||||||
|
|
||||||
const s3client = new S3Client(s3Options);
|
|
||||||
|
|
||||||
const contents: _content[] = [];
|
|
||||||
let s3ContinuationToken: string | undefined | null = null;
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
const param = {
|
|
||||||
Bucket: s3BucketName
|
|
||||||
} as ListObjectsV2CommandInput;
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
core.debug(`ListObjects Count: ${count}`);
|
|
||||||
if (s3ContinuationToken != null) {
|
|
||||||
param.ContinuationToken = s3ContinuationToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: ListObjectsV2CommandOutput;
|
|
||||||
try {
|
|
||||||
response = await s3client.send(new ListObjectsV2Command(param));
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Error from S3: ${e}`);
|
|
||||||
}
|
|
||||||
if (!response.Contents) {
|
|
||||||
return null;
|
|
||||||
// throw new Error(`Cannot found object in bucket ${s3BucketName}`);
|
|
||||||
}
|
|
||||||
core.debug(`Found objects ${response.Contents.length}`);
|
|
||||||
|
|
||||||
const found = response.Contents.find(
|
|
||||||
(content: _Object) => content.Key === primaryKey
|
|
||||||
);
|
|
||||||
if (found && found.LastModified) {
|
|
||||||
return {
|
|
||||||
cacheKey: primaryKey,
|
|
||||||
creationTime: found.LastModified.toString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Contents.map((obj: _Object) =>
|
|
||||||
contents.push({
|
|
||||||
Key: obj.Key,
|
|
||||||
LastModified: obj.LastModified
|
|
||||||
})
|
|
||||||
);
|
|
||||||
core.debug(`Total objects ${contents.length}`);
|
|
||||||
|
|
||||||
if (response.IsTruncated) {
|
|
||||||
s3ContinuationToken = response.NextContinuationToken;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
core.debug("Not found in primary key, will fallback to restore keys");
|
|
||||||
const notPrimaryKey = keys.slice(1);
|
|
||||||
const found = searchRestoreKeyEntry(notPrimaryKey, contents);
|
|
||||||
if (found != null && found.LastModified) {
|
|
||||||
return {
|
|
||||||
cacheKey: found.Key,
|
|
||||||
creationTime: found.LastModified.toString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function searchRestoreKeyEntry(
|
|
||||||
notPrimaryKey: string[],
|
|
||||||
entries: _content[]
|
|
||||||
): _content | null {
|
|
||||||
for (const k of notPrimaryKey) {
|
|
||||||
const found = _searchRestoreKeyEntry(k, entries);
|
|
||||||
if (found != null) {
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _searchRestoreKeyEntry(
|
|
||||||
notPrimaryKey: string,
|
|
||||||
entries: _content[]
|
|
||||||
): _content | null {
|
|
||||||
const matchPrefix: _content[] = [];
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.Key === notPrimaryKey) {
|
|
||||||
// extractly match, Use this entry
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.Key?.startsWith(notPrimaryKey)) {
|
|
||||||
matchPrefix.push(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchPrefix.length === 0) {
|
|
||||||
// not found, go to next key
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
matchPrefix.sort(function (i, j) {
|
|
||||||
if (i.LastModified == undefined || j.LastModified == undefined) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (i.LastModified?.getTime() === j.LastModified?.getTime()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (i.LastModified?.getTime() > j.LastModified?.getTime()) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (i.LastModified?.getTime() < j.LastModified?.getTime()) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
// return newest entry
|
|
||||||
return matchPrefix[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCacheEntry(
|
|
||||||
keys: string[],
|
|
||||||
paths: string[],
|
|
||||||
options?: InternalCacheOptions,
|
|
||||||
s3Options?: S3ClientConfig,
|
|
||||||
s3BucketName?: string
|
|
||||||
): Promise<ArtifactCacheEntry | null> {
|
|
||||||
if (s3Options && s3BucketName) {
|
|
||||||
return await getCacheEntryS3(s3Options, s3BucketName, keys, paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
const httpClient = createHttpClient();
|
|
||||||
const version = getCacheVersion(paths, options?.compressionMethod);
|
|
||||||
const resource = `cache?keys=${encodeURIComponent(
|
|
||||||
keys.join(",")
|
|
||||||
)}&version=${version}`;
|
|
||||||
|
|
||||||
const response = await retryTypedResponse("getCacheEntry", async () =>
|
|
||||||
httpClient.getJson<ArtifactCacheEntry>(getCacheApiUrl(resource))
|
|
||||||
);
|
|
||||||
if (response.statusCode === 204) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!isSuccessStatusCode(response.statusCode)) {
|
|
||||||
throw new Error(`Cache service responded with ${response.statusCode}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cacheResult = response.result;
|
|
||||||
const cacheDownloadUrl = cacheResult?.archiveLocation;
|
|
||||||
if (!cacheDownloadUrl) {
|
|
||||||
throw new Error("Cache not found.");
|
|
||||||
}
|
|
||||||
core.setSecret(cacheDownloadUrl);
|
|
||||||
core.debug(`Cache Result:`);
|
|
||||||
core.debug(JSON.stringify(cacheResult));
|
|
||||||
|
|
||||||
return cacheResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function downloadCache(
|
|
||||||
cacheEntry: ArtifactCacheEntry,
|
|
||||||
archivePath: string,
|
|
||||||
options?: DownloadOptions,
|
|
||||||
s3Options?: S3ClientConfig,
|
|
||||||
s3BucketName?: string
|
|
||||||
): Promise<void> {
|
|
||||||
const archiveLocation = cacheEntry.archiveLocation ?? "https://example.com"; // for dummy
|
|
||||||
const archiveUrl = new URL(archiveLocation);
|
|
||||||
const downloadOptions = getDownloadOptions(options);
|
|
||||||
|
|
||||||
if (s3Options && s3BucketName && cacheEntry.cacheKey) {
|
|
||||||
await downloadCacheStorageS3(
|
|
||||||
cacheEntry.cacheKey,
|
|
||||||
archivePath,
|
|
||||||
s3Options,
|
|
||||||
s3BucketName
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Otherwise, download using the Actions http-client.
|
|
||||||
await downloadCacheHttpClient(archiveLocation, archivePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reserve Cache
|
|
||||||
export async function reserveCache(
|
|
||||||
key: string,
|
|
||||||
paths: string[],
|
|
||||||
options?: InternalCacheOptions,
|
|
||||||
s3Options?: S3ClientConfig,
|
|
||||||
s3BucketName?: string
|
|
||||||
): Promise<ITypedResponseWithError<ReserveCacheResponse>> {
|
|
||||||
if (s3Options && s3BucketName) {
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
result: null,
|
|
||||||
headers: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const httpClient = createHttpClient();
|
|
||||||
const version = getCacheVersion(paths, options?.compressionMethod);
|
|
||||||
|
|
||||||
const reserveCacheRequest: ReserveCacheRequest = {
|
|
||||||
key,
|
|
||||||
version,
|
|
||||||
cacheSize: options?.cacheSize
|
|
||||||
};
|
|
||||||
const response = await retryTypedResponse("reserveCache", async () =>
|
|
||||||
httpClient.postJson<ReserveCacheResponse>(
|
|
||||||
getCacheApiUrl("caches"),
|
|
||||||
reserveCacheRequest
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getContentRange(start: number, end: number): string {
|
|
||||||
// Format: `bytes start-end/filesize
|
|
||||||
// start and end are inclusive
|
|
||||||
// filesize can be *
|
|
||||||
// For a 200 byte chunk starting at byte 0:
|
|
||||||
// Content-Range: bytes 0-199/*
|
|
||||||
return `bytes ${start}-${end}/*`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uploadChunk(
|
|
||||||
httpClient: HttpClient,
|
|
||||||
resourceUrl: string,
|
|
||||||
openStream: () => NodeJS.ReadableStream,
|
|
||||||
start: number,
|
|
||||||
end: number
|
|
||||||
): Promise<void> {
|
|
||||||
core.debug(
|
|
||||||
`Uploading chunk of size ${
|
|
||||||
end - start + 1
|
|
||||||
} bytes at offset ${start} with content range: ${getContentRange(
|
|
||||||
start,
|
|
||||||
end
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
const additionalHeaders = {
|
|
||||||
"Content-Type": "application/octet-stream",
|
|
||||||
"Content-Range": getContentRange(start, end)
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadChunkResponse = await retryHttpClientResponse(
|
|
||||||
`uploadChunk (start: ${start}, end: ${end})`,
|
|
||||||
async () =>
|
|
||||||
httpClient.sendStream(
|
|
||||||
"PATCH",
|
|
||||||
resourceUrl,
|
|
||||||
openStream(),
|
|
||||||
additionalHeaders
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isSuccessStatusCode(uploadChunkResponse.message.statusCode)) {
|
|
||||||
throw new Error(
|
|
||||||
`Cache service responded with ${uploadChunkResponse.message.statusCode} during upload chunk.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uploadFileS3(
|
|
||||||
s3options: S3ClientConfig,
|
|
||||||
s3BucketName: string,
|
|
||||||
archivePath: string,
|
|
||||||
key: string,
|
|
||||||
concurrency: number,
|
|
||||||
maxChunkSize: number
|
|
||||||
): Promise<void> {
|
|
||||||
core.debug(`Start upload to S3 (bucket: ${s3BucketName})`);
|
|
||||||
|
|
||||||
const fileStream = fs.createReadStream(archivePath);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parallelUpload = new Upload({
|
|
||||||
client: new S3Client(s3options),
|
|
||||||
queueSize: concurrency,
|
|
||||||
partSize: maxChunkSize,
|
|
||||||
|
|
||||||
params: {
|
|
||||||
Bucket: s3BucketName,
|
|
||||||
Key: key,
|
|
||||||
Body: fileStream
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
parallelUpload.on("httpUploadProgress", (progress: Progress) => {
|
|
||||||
core.debug(`Uploading chunk progress: ${JSON.stringify(progress)}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
await parallelUpload.done();
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Cache upload failed because ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uploadFile(
|
|
||||||
httpClient: HttpClient,
|
|
||||||
cacheId: number,
|
|
||||||
archivePath: string,
|
|
||||||
key: string,
|
|
||||||
options?: UploadOptions,
|
|
||||||
s3options?: S3ClientConfig,
|
|
||||||
s3BucketName?: string
|
|
||||||
): Promise<void> {
|
|
||||||
// Upload Chunks
|
|
||||||
const uploadOptions = getUploadOptions(options);
|
|
||||||
|
|
||||||
const concurrency = utils.assertDefined(
|
|
||||||
"uploadConcurrency",
|
|
||||||
uploadOptions.uploadConcurrency
|
|
||||||
);
|
|
||||||
const maxChunkSize = utils.assertDefined(
|
|
||||||
"uploadChunkSize",
|
|
||||||
uploadOptions.uploadChunkSize
|
|
||||||
);
|
|
||||||
|
|
||||||
const parallelUploads = [...new Array(concurrency).keys()];
|
|
||||||
core.debug("Awaiting all uploads");
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
if (s3options && s3BucketName) {
|
|
||||||
await uploadFileS3(
|
|
||||||
s3options,
|
|
||||||
s3BucketName,
|
|
||||||
archivePath,
|
|
||||||
key,
|
|
||||||
concurrency,
|
|
||||||
maxChunkSize
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileSize = utils.getArchiveFileSizeInBytes(archivePath);
|
|
||||||
const resourceUrl = getCacheApiUrl(`caches/${cacheId.toString()}`);
|
|
||||||
const fd = fs.openSync(archivePath, "r");
|
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
parallelUploads.map(async () => {
|
|
||||||
while (offset < fileSize) {
|
|
||||||
const chunkSize = Math.min(fileSize - offset, maxChunkSize);
|
|
||||||
const start = offset;
|
|
||||||
const end = offset + chunkSize - 1;
|
|
||||||
offset += maxChunkSize;
|
|
||||||
|
|
||||||
await uploadChunk(
|
|
||||||
httpClient,
|
|
||||||
resourceUrl,
|
|
||||||
() =>
|
|
||||||
fs
|
|
||||||
.createReadStream(archivePath, {
|
|
||||||
fd,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
autoClose: false
|
|
||||||
})
|
|
||||||
.on("error", error => {
|
|
||||||
throw new Error(
|
|
||||||
`Cache upload failed because file read failed with ${error.message}`
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
start,
|
|
||||||
end
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
fs.closeSync(fd);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function commitCache(
|
|
||||||
httpClient: HttpClient,
|
|
||||||
cacheId: number,
|
|
||||||
filesize: number
|
|
||||||
): Promise<TypedResponse<null>> {
|
|
||||||
const commitCacheRequest: CommitCacheRequest = { size: filesize };
|
|
||||||
return await retryTypedResponse("commitCache", async () =>
|
|
||||||
httpClient.postJson<null>(
|
|
||||||
getCacheApiUrl(`caches/${cacheId.toString()}`),
|
|
||||||
commitCacheRequest
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveCache(
|
|
||||||
cacheId: number,
|
|
||||||
archivePath: string,
|
|
||||||
key: string,
|
|
||||||
options?: UploadOptions,
|
|
||||||
s3Options?: S3ClientConfig,
|
|
||||||
s3BucketName?: string
|
|
||||||
): Promise<void> {
|
|
||||||
const httpClient = createHttpClient();
|
|
||||||
|
|
||||||
core.debug("Upload cache");
|
|
||||||
await uploadFile(
|
|
||||||
httpClient,
|
|
||||||
cacheId,
|
|
||||||
archivePath,
|
|
||||||
key,
|
|
||||||
options,
|
|
||||||
s3Options,
|
|
||||||
s3BucketName
|
|
||||||
);
|
|
||||||
|
|
||||||
// Commit Cache
|
|
||||||
core.debug("Commiting cache");
|
|
||||||
const cacheSize = utils.getArchiveFileSizeInBytes(archivePath);
|
|
||||||
core.info(
|
|
||||||
`Cache Size: ~${Math.round(
|
|
||||||
cacheSize / (1024 * 1024)
|
|
||||||
)} MB (${cacheSize} B)`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!s3Options) {
|
|
||||||
// already commit on S3
|
|
||||||
const commitCacheResponse = await commitCache(
|
|
||||||
httpClient,
|
|
||||||
cacheId,
|
|
||||||
cacheSize
|
|
||||||
);
|
|
||||||
if (!isSuccessStatusCode(commitCacheResponse.statusCode)) {
|
|
||||||
throw new Error(
|
|
||||||
`Cache service responded with ${commitCacheResponse.statusCode} during commit cache.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info("Cache saved successfully");
|
|
||||||
}
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
import { HttpClientError } from "@actions/http-client";
|
|
||||||
import { TypedResponse } from "@actions/http-client/lib/interfaces";
|
|
||||||
|
|
||||||
import { CompressionMethod } from "./cacheUtils";
|
|
||||||
|
|
||||||
export interface ITypedResponseWithError<T> extends TypedResponse<T> {
|
|
||||||
error?: HttpClientError;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ArtifactCacheEntry {
|
|
||||||
cacheKey?: string;
|
|
||||||
scope?: string;
|
|
||||||
creationTime?: string;
|
|
||||||
archiveLocation?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CommitCacheRequest {
|
|
||||||
size: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReserveCacheRequest {
|
|
||||||
key: string;
|
|
||||||
version?: string;
|
|
||||||
cacheSize?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReserveCacheResponse {
|
|
||||||
cacheId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InternalCacheOptions {
|
|
||||||
compressionMethod?: CompressionMethod;
|
|
||||||
cacheSize?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options to control cache upload
|
|
||||||
*/
|
|
||||||
export interface UploadOptions {
|
|
||||||
/**
|
|
||||||
* Number of parallel cache upload
|
|
||||||
*
|
|
||||||
* @default 4
|
|
||||||
*/
|
|
||||||
uploadConcurrency?: number;
|
|
||||||
/**
|
|
||||||
* Maximum chunk size in bytes for cache upload
|
|
||||||
*
|
|
||||||
* @default 32MB
|
|
||||||
*/
|
|
||||||
uploadChunkSize?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options to control cache download
|
|
||||||
*/
|
|
||||||
export interface DownloadOptions {
|
|
||||||
/**
|
|
||||||
* Indicates whether to use the Azure Blob SDK to download caches
|
|
||||||
* that are stored on Azure Blob Storage to improve reliability and
|
|
||||||
* performance
|
|
||||||
*
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
useAzureSdk?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of parallel downloads (this option only applies when using
|
|
||||||
* the Azure SDK)
|
|
||||||
*
|
|
||||||
* @default 8
|
|
||||||
*/
|
|
||||||
downloadConcurrency?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum time for each download request, in milliseconds (this
|
|
||||||
* option only applies when using the Azure SDK)
|
|
||||||
*
|
|
||||||
* @default 30000
|
|
||||||
*/
|
|
||||||
timeoutInMs?: number;
|
|
||||||
|
|
||||||
lookupOnly?: boolean;
|
|
||||||
}
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
import * as core from "@actions/core";
|
|
||||||
import { HttpClient, HttpClientResponse } from "@actions/http-client";
|
|
||||||
import { GetObjectCommand, S3Client, S3ClientConfig } from "@aws-sdk/client-s3";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import * as stream from "stream";
|
|
||||||
import * as util from "util";
|
|
||||||
|
|
||||||
import * as utils from "./cacheUtils";
|
|
||||||
import { SocketTimeout } from "./cacheUtils";
|
|
||||||
import { retryHttpClientResponse } from "./requestUtils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pipes the body of a HTTP response to a stream
|
|
||||||
*
|
|
||||||
* @param response the HTTP response
|
|
||||||
* @param output the writable stream
|
|
||||||
*/
|
|
||||||
async function pipeResponseToStream(
|
|
||||||
response: HttpClientResponse,
|
|
||||||
output: NodeJS.WritableStream
|
|
||||||
): Promise<void> {
|
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
|
||||||
await pipeline(response.message, output);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download the cache using the Actions toolkit http-client
|
|
||||||
*
|
|
||||||
* @param archiveLocation the URL for the cache
|
|
||||||
* @param archivePath the local path where the cache is saved
|
|
||||||
*/
|
|
||||||
export async function downloadCacheHttpClient(
|
|
||||||
archiveLocation: string,
|
|
||||||
archivePath: string
|
|
||||||
): Promise<void> {
|
|
||||||
const writeStream = fs.createWriteStream(archivePath);
|
|
||||||
const httpClient = new HttpClient("actions/cache");
|
|
||||||
const downloadResponse = await retryHttpClientResponse(
|
|
||||||
"downloadCache",
|
|
||||||
async () => httpClient.get(archiveLocation)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Abort download if no traffic received over the socket.
|
|
||||||
downloadResponse.message.socket.setTimeout(SocketTimeout, () => {
|
|
||||||
downloadResponse.message.destroy();
|
|
||||||
core.debug(
|
|
||||||
`Aborting download, socket timed out after ${SocketTimeout} ms`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await pipeResponseToStream(downloadResponse, writeStream);
|
|
||||||
|
|
||||||
// Validate download size.
|
|
||||||
const contentLengthHeader =
|
|
||||||
downloadResponse.message.headers["content-length"];
|
|
||||||
|
|
||||||
if (contentLengthHeader) {
|
|
||||||
const expectedLength = parseInt(contentLengthHeader);
|
|
||||||
const actualLength = utils.getArchiveFileSizeInBytes(archivePath);
|
|
||||||
|
|
||||||
if (actualLength !== expectedLength) {
|
|
||||||
throw new Error(
|
|
||||||
`Incomplete download. Expected file size: ${expectedLength}, actual file size: ${actualLength}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
core.debug("Unable to validate download, no Content-Length header");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download the cache using the AWS S3. Only call this method if the use S3.
|
|
||||||
*
|
|
||||||
* @param key the key for the cache in S3
|
|
||||||
* @param archivePath the local path where the cache is saved
|
|
||||||
* @param s3Options: the option for AWS S3 client
|
|
||||||
* @param s3BucketName: the name of bucket in AWS S3
|
|
||||||
*/
|
|
||||||
export async function downloadCacheStorageS3(
|
|
||||||
key: string,
|
|
||||||
archivePath: string,
|
|
||||||
s3Options: S3ClientConfig,
|
|
||||||
s3BucketName: string
|
|
||||||
): Promise<void> {
|
|
||||||
const s3client = new S3Client(s3Options);
|
|
||||||
const param = {
|
|
||||||
Bucket: s3BucketName,
|
|
||||||
Key: key
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await s3client.send(new GetObjectCommand(param));
|
|
||||||
if (!response.Body) {
|
|
||||||
throw new Error(
|
|
||||||
`Incomplete download. response.Body is undefined from S3.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileStream = fs.createWriteStream(archivePath);
|
|
||||||
|
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
|
||||||
await pipeline(response.Body as stream.Readable, fileStream);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
import * as core from "@actions/core";
|
|
||||||
import { getProxyUrl } from "@actions/http-client";
|
|
||||||
import { S3ClientConfig } from "@aws-sdk/client-s3";
|
|
||||||
import { NodeHttpHandler } from "@aws-sdk/node-http-handler";
|
|
||||||
import ProxyAgent from "proxy-agent";
|
|
||||||
|
|
||||||
import { Inputs } from "../constants";
|
|
||||||
import { DownloadOptions, UploadOptions } from "./contracts";
|
|
||||||
|
|
||||||
export function getConfig(): S3ClientConfig {
|
|
||||||
const proxy = getProxyUrl("https://amazonaws.com");
|
|
||||||
|
|
||||||
const config: S3ClientConfig = {
|
|
||||||
credentials: {
|
|
||||||
accessKeyId: core.getInput(Inputs.AwsAccessKeyId),
|
|
||||||
secretAccessKey: core.getInput(Inputs.AwsSecretAccessKey)
|
|
||||||
},
|
|
||||||
region: core.getInput(Inputs.AwsRegion)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (proxy) {
|
|
||||||
config.requestHandler = new NodeHttpHandler({
|
|
||||||
httpsAgent: ProxyAgent(proxy)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a copy of the upload options with defaults filled in.
|
|
||||||
*
|
|
||||||
* @param copy the original upload options
|
|
||||||
*/
|
|
||||||
export function getUploadOptions(copy?: UploadOptions): UploadOptions {
|
|
||||||
const result: UploadOptions = {
|
|
||||||
uploadConcurrency: 4,
|
|
||||||
uploadChunkSize: 32 * 1024 * 1024
|
|
||||||
};
|
|
||||||
|
|
||||||
if (copy) {
|
|
||||||
if (typeof copy.uploadConcurrency === "number") {
|
|
||||||
result.uploadConcurrency = copy.uploadConcurrency;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof copy.uploadChunkSize === "number") {
|
|
||||||
result.uploadChunkSize = copy.uploadChunkSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
core.debug(`Upload concurrency: ${result.uploadConcurrency}`);
|
|
||||||
core.debug(`Upload chunk size: ${result.uploadChunkSize}`);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a copy of the download options with defaults filled in.
|
|
||||||
*
|
|
||||||
* @param copy the original download options
|
|
||||||
*/
|
|
||||||
export function getDownloadOptions(copy?: DownloadOptions): DownloadOptions {
|
|
||||||
const result: DownloadOptions = {
|
|
||||||
useAzureSdk: true,
|
|
||||||
downloadConcurrency: 8,
|
|
||||||
timeoutInMs: 30000
|
|
||||||
};
|
|
||||||
|
|
||||||
if (copy) {
|
|
||||||
if (typeof copy.useAzureSdk === "boolean") {
|
|
||||||
result.useAzureSdk = copy.useAzureSdk;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof copy.downloadConcurrency === "number") {
|
|
||||||
result.downloadConcurrency = copy.downloadConcurrency;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof copy.timeoutInMs === "number") {
|
|
||||||
result.timeoutInMs = copy.timeoutInMs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
core.debug(`Use Azure SDK: ${result.useAzureSdk}`);
|
|
||||||
core.debug(`Download concurrency: ${result.downloadConcurrency}`);
|
|
||||||
core.debug(`Request timeout (ms): ${result.timeoutInMs}`);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
import * as core from "@actions/core";
|
|
||||||
import {
|
|
||||||
HttpClientError,
|
|
||||||
HttpClientResponse,
|
|
||||||
HttpCodes
|
|
||||||
} from "@actions/http-client";
|
|
||||||
|
|
||||||
import { DefaultRetryAttempts, DefaultRetryDelay } from "./cacheUtils";
|
|
||||||
import { ITypedResponseWithError } from "./contracts";
|
|
||||||
|
|
||||||
export function isSuccessStatusCode(statusCode?: number): boolean {
|
|
||||||
if (!statusCode) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return statusCode >= 200 && statusCode < 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isServerErrorStatusCode(statusCode?: number): boolean {
|
|
||||||
if (!statusCode) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return statusCode >= 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isRetryableStatusCode(statusCode?: number): boolean {
|
|
||||||
if (!statusCode) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const retryableStatusCodes = [
|
|
||||||
HttpCodes.BadGateway,
|
|
||||||
HttpCodes.ServiceUnavailable,
|
|
||||||
HttpCodes.GatewayTimeout
|
|
||||||
];
|
|
||||||
return retryableStatusCodes.includes(statusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sleep(milliseconds: number): Promise<void> {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function retry<T>(
|
|
||||||
name: string,
|
|
||||||
method: () => Promise<T>,
|
|
||||||
getStatusCode: (arg0: T) => number | undefined,
|
|
||||||
maxAttempts = DefaultRetryAttempts,
|
|
||||||
delay = DefaultRetryDelay,
|
|
||||||
onError: ((arg0: Error) => T | undefined) | undefined = undefined
|
|
||||||
): Promise<T> {
|
|
||||||
let errorMessage = "";
|
|
||||||
let attempt = 1;
|
|
||||||
|
|
||||||
while (attempt <= maxAttempts) {
|
|
||||||
let response: T | undefined = undefined;
|
|
||||||
let statusCode: number | undefined = undefined;
|
|
||||||
let isRetryable = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
response = await method();
|
|
||||||
} catch (error: any) {
|
|
||||||
if (onError) {
|
|
||||||
response = onError(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
isRetryable = true;
|
|
||||||
errorMessage = error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
statusCode = getStatusCode(response);
|
|
||||||
|
|
||||||
if (!isServerErrorStatusCode(statusCode)) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusCode) {
|
|
||||||
isRetryable = isRetryableStatusCode(statusCode);
|
|
||||||
errorMessage = `Cache service responded with ${statusCode}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
core.debug(
|
|
||||||
`${name} - Attempt ${attempt} of ${maxAttempts} failed with error: ${errorMessage}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isRetryable) {
|
|
||||||
core.debug(`${name} - Error is not retryable`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
await sleep(delay);
|
|
||||||
attempt++;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw Error(`${name} failed: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function retryTypedResponse<T>(
|
|
||||||
name: string,
|
|
||||||
method: () => Promise<ITypedResponseWithError<T>>,
|
|
||||||
maxAttempts = DefaultRetryAttempts,
|
|
||||||
delay = DefaultRetryDelay
|
|
||||||
): Promise<ITypedResponseWithError<T>> {
|
|
||||||
return await retry(
|
|
||||||
name,
|
|
||||||
method,
|
|
||||||
(response: ITypedResponseWithError<T>) => response.statusCode,
|
|
||||||
maxAttempts,
|
|
||||||
delay,
|
|
||||||
// If the error object contains the statusCode property, extract it and return
|
|
||||||
// an TypedResponse<T> so it can be processed by the retry logic.
|
|
||||||
(error: Error) => {
|
|
||||||
if (error instanceof HttpClientError) {
|
|
||||||
return {
|
|
||||||
statusCode: error.statusCode,
|
|
||||||
result: null,
|
|
||||||
headers: {},
|
|
||||||
error
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function retryHttpClientResponse(
|
|
||||||
name: string,
|
|
||||||
method: () => Promise<HttpClientResponse>,
|
|
||||||
maxAttempts = DefaultRetryAttempts,
|
|
||||||
delay = DefaultRetryDelay
|
|
||||||
): Promise<HttpClientResponse> {
|
|
||||||
return await retry(
|
|
||||||
name,
|
|
||||||
method,
|
|
||||||
(response: HttpClientResponse) => response.message.statusCode,
|
|
||||||
maxAttempts,
|
|
||||||
delay
|
|
||||||
);
|
|
||||||
}
|
|
||||||
158
src/utils/tar.ts
158
src/utils/tar.ts
@ -1,158 +0,0 @@
|
|||||||
import { exec } from "@actions/exec";
|
|
||||||
import * as io from "@actions/io";
|
|
||||||
import { existsSync, writeFileSync } from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
|
|
||||||
import * as utils from "./cacheUtils";
|
|
||||||
import { CompressionMethod } from "./cacheUtils";
|
|
||||||
|
|
||||||
async function getTarPath(
|
|
||||||
args: string[],
|
|
||||||
compressionMethod: CompressionMethod
|
|
||||||
): Promise<string> {
|
|
||||||
switch (process.platform) {
|
|
||||||
case "win32": {
|
|
||||||
const systemTar = `${process.env["windir"]}\\System32\\tar.exe`;
|
|
||||||
if (compressionMethod !== CompressionMethod.Gzip) {
|
|
||||||
// We only use zstandard compression on windows when gnu tar is installed due to
|
|
||||||
// a bug with compressing large files with bsdtar + zstd
|
|
||||||
args.push("--force-local");
|
|
||||||
} else if (existsSync(systemTar)) {
|
|
||||||
return systemTar;
|
|
||||||
} else if (await utils.isGnuTarInstalled()) {
|
|
||||||
args.push("--force-local");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "darwin": {
|
|
||||||
const gnuTar = await io.which("gtar", false);
|
|
||||||
if (gnuTar) {
|
|
||||||
// fix permission denied errors when extracting BSD tar archive with GNU tar - https://github.com/actions/cache/issues/527
|
|
||||||
args.push("--delay-directory-restore");
|
|
||||||
return gnuTar;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return await io.which("tar", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function execTar(
|
|
||||||
args: string[],
|
|
||||||
compressionMethod: CompressionMethod,
|
|
||||||
cwd?: string
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
await exec(`"${await getTarPath(args, compressionMethod)}"`, args, {
|
|
||||||
cwd
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(`Tar failed with error: ${error?.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWorkingDirectory(): string {
|
|
||||||
return process.env["GITHUB_WORKSPACE"] ?? process.cwd();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function extractTar(
|
|
||||||
archivePath: string,
|
|
||||||
compressionMethod: CompressionMethod
|
|
||||||
): Promise<void> {
|
|
||||||
// Create directory to extract tar into
|
|
||||||
const workingDirectory = getWorkingDirectory();
|
|
||||||
await io.mkdirP(workingDirectory);
|
|
||||||
// --d: Decompress.
|
|
||||||
// --long=#: Enables long distance matching with # bits. Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit.
|
|
||||||
// Using 30 here because we also support 32-bit self-hosted runners.
|
|
||||||
function getCompressionProgram(): string[] {
|
|
||||||
switch (compressionMethod) {
|
|
||||||
case CompressionMethod.Zstd:
|
|
||||||
return ["--use-compress-program", "zstd -d --long=30"];
|
|
||||||
case CompressionMethod.ZstdWithoutLong:
|
|
||||||
return ["--use-compress-program", "zstd -d"];
|
|
||||||
default:
|
|
||||||
return ["-z"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const args = [
|
|
||||||
...getCompressionProgram(),
|
|
||||||
"-xf",
|
|
||||||
archivePath.replace(new RegExp(`\\${path.sep}`, "g"), "/"),
|
|
||||||
"-P",
|
|
||||||
"-C",
|
|
||||||
workingDirectory.replace(new RegExp(`\\${path.sep}`, "g"), "/")
|
|
||||||
];
|
|
||||||
await execTar(args, compressionMethod);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createTar(
|
|
||||||
archiveFolder: string,
|
|
||||||
sourceDirectories: string[],
|
|
||||||
compressionMethod: CompressionMethod
|
|
||||||
): Promise<void> {
|
|
||||||
// Write source directories to manifest.txt to avoid command length limits
|
|
||||||
const manifestFilename = "manifest.txt";
|
|
||||||
const cacheFileName = utils.getCacheFileName(compressionMethod);
|
|
||||||
writeFileSync(
|
|
||||||
path.join(archiveFolder, manifestFilename),
|
|
||||||
sourceDirectories.join("\n")
|
|
||||||
);
|
|
||||||
const workingDirectory = getWorkingDirectory();
|
|
||||||
|
|
||||||
// -T#: Compress using # working thread. If # is 0, attempt to detect and use the number of physical CPU cores.
|
|
||||||
// --long=#: Enables long distance matching with # bits. Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit.
|
|
||||||
// Using 30 here because we also support 32-bit self-hosted runners.
|
|
||||||
// Long range mode is added to zstd in v1.3.2 release, so we will not use --long in older version of zstd.
|
|
||||||
function getCompressionProgram(): string[] {
|
|
||||||
switch (compressionMethod) {
|
|
||||||
case CompressionMethod.Zstd:
|
|
||||||
return ["--use-compress-program", "zstd -T0 --long=30"];
|
|
||||||
case CompressionMethod.ZstdWithoutLong:
|
|
||||||
return ["--use-compress-program", "zstd -T0"];
|
|
||||||
default:
|
|
||||||
return ["-z"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const args = [
|
|
||||||
"--posix",
|
|
||||||
...getCompressionProgram(),
|
|
||||||
"-cf",
|
|
||||||
cacheFileName.replace(new RegExp(`\\${path.sep}`, "g"), "/"),
|
|
||||||
"-P",
|
|
||||||
"-C",
|
|
||||||
workingDirectory.replace(new RegExp(`\\${path.sep}`, "g"), "/"),
|
|
||||||
"--files-from",
|
|
||||||
manifestFilename
|
|
||||||
];
|
|
||||||
await execTar(args, compressionMethod, archiveFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listTar(
|
|
||||||
archivePath: string,
|
|
||||||
compressionMethod: CompressionMethod
|
|
||||||
): Promise<void> {
|
|
||||||
// --d: Decompress.
|
|
||||||
// --long=#: Enables long distance matching with # bits.
|
|
||||||
// Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit.
|
|
||||||
// Using 30 here because we also support 32-bit self-hosted runners.
|
|
||||||
function getCompressionProgram(): string[] {
|
|
||||||
switch (compressionMethod) {
|
|
||||||
case CompressionMethod.Zstd:
|
|
||||||
return ["--use-compress-program", "zstd -d --long=30"];
|
|
||||||
case CompressionMethod.ZstdWithoutLong:
|
|
||||||
return ["--use-compress-program", "zstd -d"];
|
|
||||||
default:
|
|
||||||
return ["-z"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const args = [
|
|
||||||
...getCompressionProgram(),
|
|
||||||
"-tf",
|
|
||||||
archivePath.replace(new RegExp(`\\${path.sep}`, "g"), "/"),
|
|
||||||
"-P"
|
|
||||||
];
|
|
||||||
await execTar(args, compressionMethod);
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user