Skip to content

Commit ac40596

Browse files
author
Cole Kennedy
committed
Initial commit of action-wrapper
0 parents  commit ac40596

File tree

6 files changed

+218
-0
lines changed

6 files changed

+218
-0
lines changed

.github/workflows/test.yml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Test Wrapper Action
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout repository
14+
uses: actions/checkout@v3
15+
16+
- name: Setup Node.js
17+
uses: actions/setup-node@v3
18+
with:
19+
node-version: '16'
20+
21+
- name: Install dependencies
22+
run: npm ci
23+
24+
- name: Test wrapper with a simple action
25+
uses: ./
26+
with:
27+
action-ref: "actions/hello-world-javascript-action@main"

.gitignore

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Dependency directories
2+
node_modules/
3+
npm-debug.log
4+
yarn-debug.log
5+
yarn-error.log
6+
7+
# Distribution directories
8+
dist/
9+
lib/
10+
11+
# OS specific files
12+
.DS_Store
13+
Thumbs.db
14+
15+
# Editor directories and files
16+
.idea
17+
.vscode
18+
*.swp
19+
*.swo

README.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# TestifySec Action Wrapper
2+
3+
A GitHub Action that downloads and executes another GitHub Action dynamically.
4+
5+
## Usage
6+
7+
```yaml
8+
name: Example Workflow
9+
on: [push, pull_request]
10+
11+
jobs:
12+
build:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v3
17+
- name: Run Nested Action via Wrapper
18+
uses: testifysec/action-wrapper@v1
19+
with:
20+
action-ref: "owner/[email protected]"
21+
extra-args: "--foo bar"
22+
```
23+
24+
## Inputs
25+
26+
| Input | Description | Required |
27+
|-------|-------------|----------|
28+
| `action-ref` | Reference to the nested action (e.g., owner/repo@ref) | Yes |
29+
| `extra-args` | Extra arguments to pass to the nested action | No |
30+
31+
## How It Works
32+
33+
1. **Parsing the Input:**
34+
The wrapper reads an input `action-ref` (like `"owner/repo@ref"`) and splits it into the repository identifier and ref.
35+
36+
2. **Downloading the Repository:**
37+
It constructs the URL for the GitHub zip archive and downloads it using Axios. The zip is then extracted using the `unzipper` package into a temporary directory.
38+
39+
3. **Reading Action Metadata:**
40+
The script reads the `action.yml` file from the extracted folder to determine the JavaScript entry point (from the `runs.main` field).
41+
42+
4. **Dependency Installation (Optional):**
43+
If a `package.json` is present in the nested action, it runs `npm install` to install dependencies.
44+
45+
5. **Executing the Nested Action:**
46+
Finally, the wrapper runs the nested action's entry file using Node.js. Any extra arguments provided via the `extra-args` input are passed along.

action.yml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name: "TestifySec Action Wrapper"
2+
description: "Downloads and executes another GitHub Action"
3+
inputs:
4+
action-ref:
5+
description: "Reference to the nested action (e.g., owner/repo@ref)"
6+
required: true
7+
extra-args:
8+
description: "Extra arguments to pass to the nested action"
9+
required: false
10+
runs:
11+
using: "node16"
12+
main: "index.js"

index.js

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
const core = require("@actions/core");
2+
const exec = require("@actions/exec");
3+
const fs = require("fs");
4+
const os = require("os");
5+
const path = require("path");
6+
const axios = require("axios");
7+
const unzipper = require("unzipper");
8+
const yaml = require("js-yaml");
9+
10+
async function run() {
11+
try {
12+
// Get inputs
13+
const actionRef = core.getInput("action-ref");
14+
const extraArgs = core.getInput("extra-args") || "";
15+
16+
// Parse action-ref (expects format: owner/repo@ref)
17+
const [repo, ref] = parseActionRef(actionRef);
18+
core.info(`Parsed repo: ${repo}, ref: ${ref}`);
19+
20+
// Construct URL for the repository zip archive
21+
const zipUrl = `https://github.com/${repo}/archive/${ref}.zip`;
22+
core.info(`Downloading action from: ${zipUrl}`);
23+
24+
// Create a temporary directory for extraction
25+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nested-action-"));
26+
27+
// Download and extract the zip archive
28+
const response = await axios({
29+
url: zipUrl,
30+
method: "GET",
31+
responseType: "stream"
32+
});
33+
await new Promise((resolve, reject) => {
34+
response.data
35+
.pipe(unzipper.Extract({ path: tempDir }))
36+
.on("close", resolve)
37+
.on("error", reject);
38+
});
39+
core.info(`Downloaded and extracted to ${tempDir}`);
40+
41+
// GitHub archives typically extract to a folder named "repo-ref"
42+
const repoName = repo.split("/")[1];
43+
const extractedFolder = path.join(tempDir, `${repoName}-${ref}`);
44+
if (!fs.existsSync(extractedFolder)) {
45+
throw new Error(`Extracted folder ${extractedFolder} not found.`);
46+
}
47+
48+
// Read action.yml from the downloaded action
49+
const actionYmlPath = path.join(extractedFolder, "action.yml");
50+
if (!fs.existsSync(actionYmlPath)) {
51+
throw new Error(`action.yml not found in ${extractedFolder}`);
52+
}
53+
const actionConfig = yaml.load(fs.readFileSync(actionYmlPath, "utf8"));
54+
const entryPoint = actionConfig.runs && actionConfig.runs.main;
55+
if (!entryPoint) {
56+
throw new Error("Entry point (runs.main) not defined in action.yml");
57+
}
58+
core.info(`Nested action entry point: ${entryPoint}`);
59+
60+
// Construct full path to the nested action's entry file
61+
const entryFile = path.join(extractedFolder, entryPoint);
62+
if (!fs.existsSync(entryFile)) {
63+
throw new Error(`Entry file ${entryFile} does not exist.`);
64+
}
65+
66+
// Optionally, install dependencies if package.json exists
67+
const pkgJsonPath = path.join(extractedFolder, "package.json");
68+
if (fs.existsSync(pkgJsonPath)) {
69+
core.info("Installing dependencies for nested action...");
70+
await exec.exec("npm", ["install"], { cwd: extractedFolder });
71+
}
72+
73+
// Execute the nested action using Node.js
74+
const args = extraArgs.split(/\s+/).filter((a) => a); // split and remove empty strings
75+
core.info(`Executing nested action: node ${entryFile} ${args.join(" ")}`);
76+
await exec.exec("node", [entryFile, ...args], { cwd: extractedFolder });
77+
78+
} catch (error) {
79+
core.setFailed(`Wrapper action failed: ${error.message}`);
80+
}
81+
}
82+
83+
function parseActionRef(refString) {
84+
const parts = refString.split("@");
85+
if (parts.length !== 2) {
86+
throw new Error("Invalid action-ref format. Expected 'owner/repo@ref'");
87+
}
88+
return parts;
89+
}
90+
91+
run();

package.json

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "action-wrapper",
3+
"version": "1.0.0",
4+
"description": "A GitHub Action that wraps other actions",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"keywords": [
10+
"github",
11+
"actions",
12+
"wrapper"
13+
],
14+
"author": "TestifySec",
15+
"license": "MIT",
16+
"dependencies": {
17+
"@actions/core": "^1.10.0",
18+
"@actions/exec": "^1.1.1",
19+
"axios": "^1.3.4",
20+
"js-yaml": "^4.1.0",
21+
"unzipper": "^0.10.11"
22+
}
23+
}

0 commit comments

Comments
 (0)