Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: download artifacts from current workflow run, improve API usage #9

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17,584 changes: 8,261 additions & 9,323 deletions dist/post/index.js

Large diffs are not rendered by default.

23,789 changes: 20,226 additions & 3,563 deletions dist/starter/index.js

Large diffs are not rendered by default.

60,197 changes: 38,083 additions & 22,114 deletions dist/turboServer/index.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
"release": "release-it --only-version"
},
"devDependencies": {
"@actions/artifact": "^0.6.1",
"@actions/artifact": "^1.1.0",
"@actions/core": "^1.6.0",
"@tsconfig/node12": "^1.0.9",
"@types/fs-extra": "^9.0.13",
"@types/node": "^17.0.16",
"@vercel/ncc": "^0.33.1",
"axios": "^0.25.0",
"axios": "^0.27.2",
"express": "^4.17.2",
"express-async-handler": "^1.2.0",
"fs-extra": "^10.0.0",
Expand Down
37 changes: 25 additions & 12 deletions src/starter.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
import { info, saveState } from '@actions/core';
import { info, saveState, setFailed } from '@actions/core';
import { spawn } from 'child_process';
import fs from 'fs-extra';
import path from 'path';
import { cacheDir, States } from './utils/constants';
import { downloadSameWorkflowArtifacts } from './utils/downloadArtifact';

fs.ensureDirSync(cacheDir);
async function main() {
await fs.ensureDir(cacheDir);

const out = fs.openSync(path.join(cacheDir, 'out.log'), 'a');
const err = fs.openSync(path.join(cacheDir, 'out.log'), 'a');
await downloadSameWorkflowArtifacts();

const subprocess = spawn('node', [path.resolve(__dirname, '../turboServer/index.js')], {
detached: true,
stdio: ['ignore', out, err],
env: process.env,
});
const out = fs.openSync(path.join(cacheDir, 'out.log'), 'a');
const err = fs.openSync(path.join(cacheDir, 'out.log'), 'a');

const subprocess = spawn(
'node',
[path.resolve(__dirname, '../turboServer/index.js')],
{
detached: true,
stdio: ['ignore', out, err],
env: process.env,
}
);

subprocess.unref();
subprocess.unref();

info(`${States.TURBO_LOCAL_SERVER_PID}: ${subprocess.pid}`);
saveState(States.TURBO_LOCAL_SERVER_PID, subprocess.pid);
info(`${States.TURBO_LOCAL_SERVER_PID}: ${subprocess.pid}`);
saveState(States.TURBO_LOCAL_SERVER_PID, subprocess.pid);
}

main().catch((error) => {
setFailed(error);
});
40 changes: 29 additions & 11 deletions src/turboServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import express from 'express';
import asyncHandler from 'express-async-handler';
import fs from 'fs-extra';
import path from 'path';
import { artifactApi } from './utils/artifactApi';
import { artifactApi, IArtifactListResponse } from './utils/artifactApi';
import { cacheDir, Inputs } from './utils/constants';
import { downloadArtifact } from './utils/downloadArtifact';

Expand All @@ -18,6 +18,8 @@ async function startServer() {
trimWhitespace: true,
});

let artifactList: IArtifactListResponse | undefined;

app.all('*', (req, res, next) => {
console.info(`Got a ${req.method} request`, req.path);
const { authorization = '' } = req.headers;
Expand All @@ -34,22 +36,33 @@ async function startServer() {
'/v8/artifacts/:artifactId',
asyncHandler(async (req: any, res: any) => {
const { artifactId } = req.params;
const list = await artifactApi.listArtifacts();

const existingArtifact = list.artifacts?.find(
(artifact) => artifact.name === artifactId
);
const filepath = path.join(cacheDir, `${artifactId}.gz`);

if (!fs.pathExistsSync(filepath)) {
// Requested artifact does not belong to the current workflow, so it wasn't downloaded before
// Check if it exists for another one
if (!artifactList) {
// Cache the response for the runtime of the server
// The server is typically short-lived, so it is very unlikely that we're going to miss an artifact this way
artifactList = await artifactApi.listArtifacts();
}

const existingArtifact = artifactList.artifacts?.find(
(artifact) => artifact.name === artifactId
);

if (existingArtifact) {
console.log(`Artifact ${artifactId} found.`);
await downloadArtifact(existingArtifact, cacheDir);
if (existingArtifact) {
console.log(`Artifact ${artifactId} found.`);
await downloadArtifact(existingArtifact, cacheDir);
}
console.log(
`Artifact ${artifactId} downloaded successfully to ${cacheDir}/${artifactId}.gz.`
`Artifact ${artifactId} downloaded successfully to ${filepath}.`
);
} else {
console.log(`Artifact ${artifactId} already exists.`);
}

const filepath = path.join(cacheDir, `${artifactId}.gz`);

if (!fs.pathExistsSync(filepath)) {
console.log(`Artifact ${artifactId} not found.`);
return res.status(404).send('Not found');
Expand Down Expand Up @@ -85,6 +98,11 @@ async function startServer() {
});
});

app.post('/v8/artifacts/events', (req, res) => {
// Ignore
res.status(200).send();
});

app.disable('etag').listen(port, () => {
console.log(`Cache dir: ${cacheDir}`);
console.log(`Local Turbo server is listening at http://127.0.0.1:${port}`);
Expand Down
2 changes: 1 addition & 1 deletion src/utils/artifactApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getInput } from '@actions/core';
import { Axios } from 'axios';
import { Inputs } from './constants';

interface IArtifactListResponse {
export interface IArtifactListResponse {
total_count: number;
artifacts?: Array<{
id: number;
Expand Down
35 changes: 35 additions & 0 deletions src/utils/downloadArtifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import StreamZip from 'node-stream-zip';
import path from 'path';
import { artifactApi } from './artifactApi';
import os from 'os';
import { create } from '@actions/artifact';
import { cacheDir } from './constants';

const tempArchiveFolder = path.join(
process.env.RUNNER_TEMP || os.tmpdir(),
Expand Down Expand Up @@ -33,3 +35,36 @@ export async function downloadArtifact(artifact, destFolder) {
await zip.extract(null, destFolder);
await zip.close();
}

export async function downloadSameWorkflowArtifacts() {
const client = create();
// Try to download all artifacts from the current workflow, but do not fail the build if this fails
const artifacts = await client.downloadAllArtifacts(cacheDir).catch((e) => {
Copy link
Owner

@felixmosh felixmosh Sep 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do you ensure that these artifacts are belongs to the current workflow?
Looks like you are downloading all the files, which may contain hundreds of gigs in my case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's this method: https://github.com/actions/toolkit/blob/e6257f111756d2f3567917c8e27ab57de8c3e09c/packages/artifact/src/internal/artifact-client.ts#L221
using this method to list artifacts: https://github.com/actions/toolkit/blob/e6257f111756d2f3567917c8e27ab57de8c3e09c/packages/artifact/src/internal/download-http-client.ts#L45
which in turn is using https://github.com/actions/toolkit/blob/e6257f111756d2f3567917c8e27ab57de8c3e09c/packages/artifact/src/internal/utils.ts#L222 as the download URL
which is referencing the current workflow run using the env variable here:
https://github.com/actions/toolkit/blob/e6257f111756d2f3567917c8e27ab57de8c3e09c/packages/artifact/src/internal/config-variables.ts#L50

Looks like you are downloading all the files, which may contain hundreds of gigs in my case.

Good point. An alternative would be using @actions/artifacts internals as I attempted here:
zwave-js/node-zwave-js@10121ae (#5050)
This way one could filter for files that look like a hash, but that might not be stable across releases of that package.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't like this approach, I first attempted to download the files from the current workflow and fall back to the ones from other workflows if that didn't work. However, it was pretty slow (20 seconds for a handful of small artifacts).

console.error(`Failed to download workflow artifacts: ${e.message}`);
return [];
});

// downloadAllArtifacts creates folder with the artifact name and puts the artifact in there
// We need to move the artifact to the destFolder, so the server can find them
for (const artifact of artifacts) {
const artifactFileName = path.join(
artifact.downloadPath,
`${artifact.artifactName}.gz`
);
try {
await fs.move(
artifactFileName,
path.join(cacheDir, `${artifact.artifactName}.gz`)
);
// Remember that this artifact was downloaded from the current workflow
await fs.createFile(
path.join(cacheDir, `${artifact.artifactName}.local`)
);
await fs.remove(artifact.downloadPath);
} catch (e: any) {
console.error(
`Failed to download artifact ${artifact.artifactName}: ${e.message}`
);
}
}
}
20 changes: 13 additions & 7 deletions src/utils/uploadArtifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,28 @@ import { create } from '@actions/artifact';
import { debug, info } from '@actions/core';
import fs from 'fs-extra';
import path from 'path';
import { artifactApi } from './artifactApi';
import { cacheDir } from './constants';

export async function uploadArtifacts() {
const list = await artifactApi.listArtifacts();
const existingArtifacts = (list.artifacts || []).map(
(artifact) => artifact.name
);

// Upload all artifacts that were not downloaded from the current workflow
// This avoids a list request for every cache miss in subsequent jobs of the same workflow
const client = create();

const files = fs.readdirSync(cacheDir);

const artifactFiles = files.filter((filename) => filename.endsWith('.gz'));
const currentWorkflowArtifacts = files
.filter((filename) => filename.endsWith('.local'))
.map((file) => path.basename(file, '.local'));

debug(`artifact files: ${JSON.stringify(artifactFiles, null, 2)}`);
debug(
`artifacts downloaded from the current workflow: ${JSON.stringify(
currentWorkflowArtifacts,
null,
2
)}`
);

const artifactsToUpload = artifactFiles
.map((artifactFilename) => {
Expand All @@ -28,7 +34,7 @@ export async function uploadArtifacts() {

return { artifactFilename, artifactId };
})
.filter(({ artifactId }) => !existingArtifacts.includes(artifactId));
.filter(({ artifactId }) => !currentWorkflowArtifacts.includes(artifactId));

if (artifactsToUpload.length) {
info(`Gonna upload ${artifactsToUpload.length} artifacts:`);
Expand Down
Loading