Skip to content

Commit

Permalink
Merge pull request #55 from githubocto/mr/ia/expose-axios-config
Browse files Browse the repository at this point in the history
feat: expose axios_config
  • Loading branch information
irealva authored Jul 22, 2021
2 parents 77ea764 + 40dfcf4 commit 416fca1
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 37 deletions.
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,29 @@ For example, if this field is set to `Bearer abc123` then the following header i
}
```

And in the action yaml:
`authorization: 'Bearer abc123'`
#### `axios_config` (optional)

Under the hood, the `http` backend uses [Axios](https://github.com/axios/axios) for data fetching. By default, Flat assumes you're interested in using the `GET` method to fetch data, but if you'd like to `POST` (e.g., sending a GraphQL query), the `axios_config` option allows you to override this behavior.

Specifically, the `axios_config` parameter should reflect a relative path to a `.json` file in your repository. This JSON file should mirror the shape of [Axios' request config parameters](https://github.com/axios/axios#request-config), with a few notable exceptions.

- `url` and `baseURL` will both be ignored, as the `http_url` specified above will take precedence.
- `headers` will be merged in with the authorization header described by the `authorization` parameter above. Please do not put secret keys here, as they will be stored in plain text!
- All `function` parameters will be ignored (e.g., `transformRequest`).
- The response type is always set to `responseType: 'stream'` in the background.
An example `axios_config` might look thusly if you were interested in hitting GitHub's GraphQL API ([here is a demo](https://github.com/githubocto/flat-demo-graphql)) 👇

```json
{
"method": "post",
"data": {
"query": "query { repository(owner:\"octocat\", name:\"Hello-World\") { issues(last:20, states:CLOSED) { edges { node { title url labels(first:5) { edges { node { name } } } } } } } }"
}
}
```

We advise escaping double quotes like `\"` in your JSON file.

#### `downloaded_filename`

Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: 'Flat Data'
description: 'The GitHub action which powers data fetching for Flat'
author: 'GitHub OCTO'
inputs:
axios_config:
description: 'A path (relative to the root of your repo) of a JSON file containing a subset of Axios configuration'
required: false
downloaded_filename:
description: 'The filename to use for writing data.'
required: false
Expand Down
48 changes: 35 additions & 13 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,36 @@ async function fetchHTTP(config) {
core.info('Fetching: HTTP');
// Authorization headers
const auth = {
headers: {
authorization: config.authorization,
}
authorization: config.authorization,
};
const headers = config.authorization ? auth : {};
const authHeader = config.authorization ? auth : {};
let response;
try {
const response = await axios_1.default.get(config.http_url, {
method: 'get',
responseType: 'stream',
...headers,
});
if (config.axios_config) {
const axiosConfig = fs_1.default.readFileSync(config.axios_config, {
encoding: 'utf8',
});
const parsed = JSON.parse(axiosConfig);
const combinedWithOtherConfigValues = {
...parsed,
url: config.http_url,
headers: {
...parsed.headers,
...authHeader,
},
responseType: 'stream',
};
response = await axios_1.default(combinedWithOtherConfigValues);
}
else {
response = await axios_1.default.get(config.http_url, {
method: 'get',
responseType: 'stream',
headers: {
...authHeader,
},
});
}
const filename = config.downloaded_filename;
const writer = fs_1.default.createWriteStream(filename);
response.data.pipe(writer);
Expand Down Expand Up @@ -249,6 +268,7 @@ const CommonConfigSchema = z.object({
});
const HTTPConfigSchema = z
.object({
axios_config: z.string().optional(),
http_url: z.string(),
authorization: z.string().optional(),
mask: z.string().optional(), // string array of secrets or boolean
Expand All @@ -265,6 +285,7 @@ const ConfigSchema = z.union([HTTPConfigSchema, SQLConfigSchema]);
function getConfig() {
const raw = {};
const keys = [
'axios_config',
'downloaded_filename',
'http_url',
'authorization',
Expand Down Expand Up @@ -480,15 +501,16 @@ async function run() {
// if including a mask config then we can strip out secrets from the http_url
sourceMasked = source; // if no secrets to mask then this is just source
if (config.mask) {
if (config.mask === 'true' || config.mask === 'false') { // mask param is a string
if (config.mask === 'true' || config.mask === 'false') {
// mask param is a string
shouldMask = JSON.parse(config.mask); // convert to boolean
}
else {
try {
const maskArray = JSON.parse(config.mask);
maskArray.forEach((secretToMask) => {
const regex = new RegExp(secretToMask, "g");
sourceMasked = sourceMasked.replace(regex, "***");
const regex = new RegExp(secretToMask, 'g');
sourceMasked = sourceMasked.replace(regex, '***');
});
}
catch (error) {
Expand All @@ -513,7 +535,7 @@ async function run() {
core.debug(`Invoking ${config.postprocess} with ${filename}...`);
try {
const raw = child_process_1.execSync(`NO_COLOR=true deno run -q --allow-read --allow-write --allow-run --allow-net --allow-env --unstable ${config.postprocess} ${filename}`).toString();
core.info("Deno output:");
core.info('Deno output:');
core.info(raw);
}
catch (error) {
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

49 changes: 36 additions & 13 deletions src/backends/http.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,50 @@
import * as core from '@actions/core'
import { HTTPConfig } from '../config'
import fs from 'fs'
import axios from 'axios'
import axios, { AxiosResponse } from 'axios'

export default async function fetchHTTP(config: HTTPConfig): Promise<string> {
core.info('Fetching: HTTP')

// Authorization headers
const auth = {
headers: {
authorization: config.authorization,
}
};
const headers = config.authorization ? auth : {}
const auth = {
authorization: config.authorization,
}
const authHeader = config.authorization ? auth : {}

let response: AxiosResponse<any>

try {
const response = await axios.get(config.http_url, {
method: 'get',
responseType: 'stream',
...headers,
})
if (config.axios_config) {
const axiosConfig = fs.readFileSync(config.axios_config, {
encoding: 'utf8',
})

const parsed = JSON.parse(axiosConfig)

const combinedWithOtherConfigValues = {
...parsed,
url: config.http_url,
headers: {
...parsed.headers,
...authHeader,
},
responseType: 'stream',
}

response = await axios(combinedWithOtherConfigValues)
} else {
response = await axios.get(config.http_url, {
method: 'get',
responseType: 'stream',
headers: {
...authHeader,
},
})
}
const filename = config.downloaded_filename
const writer = fs.createWriteStream(filename)

response.data.pipe(writer)
await new Promise((resolve, reject) => {
writer.on('finish', resolve)
Expand All @@ -32,4 +55,4 @@ export default async function fetchHTTP(config: HTTPConfig): Promise<string> {
core.setFailed(error)
throw error
}
}
}
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type CommonConfig = z.infer<typeof CommonConfigSchema>

const HTTPConfigSchema = z
.object({
axios_config: z.string().optional(),
http_url: z.string(),
authorization: z.string().optional(),
mask: z.string().optional(), // string array of secrets or boolean
Expand All @@ -34,6 +35,7 @@ export type Config = z.infer<typeof ConfigSchema>
export function getConfig(): Config {
const raw: { [k: string]: string } = {}
const keys = [
'axios_config',
'downloaded_filename',
'http_url',
'authorization',
Expand Down
18 changes: 10 additions & 8 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,20 @@ async function run(): Promise<void> {
// if including a mask config then we can strip out secrets from the http_url
sourceMasked = source // if no secrets to mask then this is just source
if (config.mask) {
if (config.mask === 'true' || config.mask === 'false') { // mask param is a string
if (config.mask === 'true' || config.mask === 'false') {
// mask param is a string
shouldMask = JSON.parse(config.mask) // convert to boolean
} else {
try {
const maskArray: string[] = JSON.parse(config.mask)
maskArray.forEach((secretToMask: string) => {
const regex = new RegExp(secretToMask, "g")
sourceMasked = sourceMasked.replace(regex, "***")
const regex = new RegExp(secretToMask, 'g')
sourceMasked = sourceMasked.replace(regex, '***')
})
} catch(error) {
core.setFailed('Mask param formatted incorrectly. It should be a string array OR a "true" or "false" string.')
} catch (error) {
core.setFailed(
'Mask param formatted incorrectly. It should be a string array OR a "true" or "false" string.'
)
}
}
}
Expand All @@ -64,9 +67,8 @@ async function run(): Promise<void> {
`NO_COLOR=true deno run -q --allow-read --allow-write --allow-run --allow-net --allow-env --unstable ${config.postprocess} ${filename}`
).toString()

core.info("Deno output:")
core.info('Deno output:')
core.info(raw)

} catch (error) {
core.setFailed(error)
}
Expand Down Expand Up @@ -117,7 +119,7 @@ async function run(): Promise<void> {

const files = [...alreadyEditedFiles, ...editedFiles]
core.exportVariable('FILES', files)

core.endGroup()
}

Expand Down

0 comments on commit 416fca1

Please sign in to comment.