refactor for cross platform use

This commit is contained in:
softprops 2019-09-09 17:10:07 +09:00
parent 090932e783
commit 18daf2c63f
15 changed files with 5626 additions and 23 deletions

View file

@ -1,9 +1,9 @@
name: Main name: Main
on: on: [pull_request, push]
push: # push:
branches: # branches:
- 'master' # - 'master'
jobs: jobs:
build: build:
@ -14,20 +14,33 @@ jobs:
uses: actions/checkout@master uses: actions/checkout@master
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Install
run: npm ci
- name: Build
run: npm run build
- name: Test
run: npm rust test
- name: "check for uncommitted changes"
# Ensure no changes, but ignore node_modules dir since dev/fresh ci deps installed.
run: |
git diff --exit-code --stat -- . ':!node_modules' \
|| (echo "##[error] found changed files after build. please 'npm run build && npm run format'" \
"and check in all changes" \
&& exit 1)
# https://github.com/actions/docker/tree/master/cli # https://github.com/actions/docker/tree/master/cli
- name: Package # - name: Package
uses: actions/docker/cli@master # uses: actions/docker/cli@master
with: # with:
args: build -t ${{ github.repository }}:latest -t ${{ github.repository }}:${{ github.sha }} . # args: build -t ${{ github.repository }}:latest -t ${{ github.repository }}:${{ github.sha }} .
# https://github.com/actions/docker/tree/master/login # # https://github.com/actions/docker/tree/master/login
- name: Publish Auth # - name: Publish Auth
uses: actions/docker/login@master # uses: actions/docker/login@master
env: # env:
# https://help.github.com/en/articles/virtual-environments-for-github-actions#creating-and-using-secrets-encrypted-variables # # https://help.github.com/en/articles/virtual-environments-for-github-actions#creating-and-using-secrets-encrypted-variables
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} # DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} # DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
# https://github.com/actions/docker/tree/master/cli # # https://github.com/actions/docker/tree/master/cli
- name: Publish # - name: Publish
uses: actions/docker/cli@master # uses: actions/docker/cli@master
with: # with:
args: push ${{ github.repository }}:latest # args: push ${{ github.repository }}:latest

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target /target
**/*.rs.bk **/*.rs.bk
node_modules

30
__tests__/github.test.ts Normal file
View file

@ -0,0 +1,30 @@
//import * as assert from "assert";
//const assert = require('assert');
import * as assert from 'assert';
import { mimeOrDefault, asset } from '../src/github';
describe('github', () => {
describe('mimeOrDefault', () => {
it('returns a specific mime for common path', async () => {
assert.equal(mimeOrDefault('foo.tar.gz'), 'application/gzip')
});
it('returns default mime for uncommon path', async () => {
assert.equal(mimeOrDefault('foo.uncommon'), 'application/octet-stream')
});
});
describe('asset', () => {
it('derives asset info from a path', async () => {
const {
name,
mime,
size,
file
} = asset('tests/data/foo/bar.txt');
assert.equal(name, 'bar.txt');
assert.equal(mime, 'text/plain');
assert.equal(size, 10);
assert.equal(file.toString(), 'release me')
});
})
});

19
__tests__/util.test.ts Normal file
View file

@ -0,0 +1,19 @@
import { isTag, paths } from '../src/util';
import * as assert from 'assert';
describe('util', () => {
describe('isTag', () => {
it('returns true for tags', async () => {
assert.equal(isTag('refs/tags/foo'), true)
});
it ('returns false for other kinds of refs', async () => {
assert.equal(isTag('refs/heads/master'), false)
})
})
describe('paths', () => {
it('resolves files given a set of paths', async () => {
assert.deepStrictEqual(paths(["tests/data/**/*"]), ['tests/data/foo/bar.txt'])
});
})
});

View file

@ -24,10 +24,12 @@ inputs:
required: false required: false
default: 'empty' default: 'empty'
runs: runs:
using: 'docker' #using: 'docker'
image: 'docker://softprops:action-gh-action' #image: 'docker://softprops:action-gh-action'
using: 'node12'
main: 'lib/main.js'
env: env:
'GITHUB_TOKEN': 'As provided by Github Actions' 'GITHUB_TOKEN': 'As provided by Github Actions'
branding: branding:
color: 'green' color: 'green'
icon: 'package' icon: 'package'

11
jest.config.js Normal file
View file

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

69
lib/github.js Normal file
View file

@ -0,0 +1,69 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = require("fs");
const mime_1 = require("mime");
const path_1 = require("path");
exports.asset = (path) => {
return {
name: path_1.basename(path),
mime: exports.mimeOrDefault(path),
size: fs_1.lstatSync(path).size,
file: fs_1.readFileSync(path)
};
};
exports.mimeOrDefault = (path) => {
return mime_1.getType(path) || "application/octet-stream";
};
exports.upload = (gh, url, path) => __awaiter(void 0, void 0, void 0, function* () {
let { name, size, mime, file } = exports.asset(path);
console.log(`⬆️ Uploading ${name}...`);
return yield gh.repos.uploadReleaseAsset({
url,
headers: {
"content-length": size,
"content-type": mime
},
name,
file
});
});
exports.release = (config, gh) => __awaiter(void 0, void 0, void 0, function* () {
let [owner, repo] = config.github_repository.split("/");
try {
let release = yield gh.repos.getReleaseByTag({
owner,
repo,
tag: config.github_ref
});
return release.data;
}
catch (error) {
if (error.status === 404) {
const tag_name = config.github_ref.replace("refs/tags/", "");
const name = config.input_name || tag_name;
const body = config.input_body;
const draft = config.input_draft;
let release = yield gh.repos.createRelease({
owner,
repo,
tag_name,
name,
body,
draft
});
return release.data;
}
else {
throw error;
}
}
});

45
lib/main.js Normal file
View file

@ -0,0 +1,45 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(require("@actions/core"));
const github_1 = require("@actions/github");
const util_1 = require("./util");
const github_2 = require("./github");
function run() {
return __awaiter(this, void 0, void 0, function* () {
try {
const config = util_1.parseConfig(process.env);
if (!util_1.isTag(config.github_ref)) {
throw new Error(`⚠️ GitHub Releases requires a tag`);
}
// todo: validate github_ref is a tag
const gh = new github_1.GitHub(config.github_token);
let rel = yield github_2.release(config, gh);
if (config.input_files) {
util_1.paths(config.input_files).forEach((path) => __awaiter(this, void 0, void 0, function* () {
yield github_2.upload(gh, rel.upload_url, path);
}));
}
console.log(`🎉 Release ready at ${rel.html_url}`);
}
catch (error) {
core.setFailed(error.message);
}
});
}
run();

31
lib/util.js Normal file
View file

@ -0,0 +1,31 @@
"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const glob = __importStar(require("glob"));
const fs_1 = require("fs");
exports.parseConfig = (env) => {
return {
github_token: env.GITHUB_TOKEN || "",
github_ref: (env.GITHUB_REF || ""),
github_repository: env.GITHUB_REPOSITORY || "",
input_name: env.INPUT_NAME,
input_body: env.INPUT_BODY,
input_body_path: env.INPUT_BODY_PATH,
input_files: (env.INPUT_FILES || "").split(","),
input_draft: env.INPUT_DRAFT === 'true'
};
};
exports.paths = (patterns) => {
return patterns.reduce((acc, pattern) => {
return acc.concat(glob.sync(pattern).filter(path => fs_1.lstatSync(path).isFile()));
}, []);
};
exports.isTag = (ref) => {
return ref.startsWith("refs/tags/");
};

5131
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

36
package.json Normal file
View file

@ -0,0 +1,36 @@
{
"name": "action-gh-release",
"version": "0.0.1",
"private": true,
"description": "GitHub Action for creating GitHub Releases",
"main": "lib/main.js",
"scripts": {
"build": "tsc",
"test": "jest"
},
"repository": {
"type": "git",
"url": "git+https://github.com/softprops/action-gh-template.git"
},
"keywords": [
"actions"
],
"author": "softprops",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.0.0",
"@actions/github": "^1.0.0",
"glob": "^7.1.4",
"mime": "^2.4.4"
},
"devDependencies": {
"@types/glob": "^7.1.1",
"@types/jest": "^24.0.13",
"@types/mime": "^2.0.1",
"@types/node": "^12.0.4",
"jest": "^24.8.0",
"jest-circus": "^24.7.1",
"ts-jest": "^24.0.2",
"typescript": "^3.5.1"
}
}

86
src/github.ts Normal file
View file

@ -0,0 +1,86 @@
import { GitHub } from '@actions/github';
import { Config } from './util';
import { lstatSync, readFileSync } from 'fs';
import { getType } from 'mime';
import { basename } from 'path';
export interface ReleaseAsset {
name: string,
mime: string,
size: number,
file: Buffer
}
export interface Release {
upload_url: string,
html_url: string
}
export const asset = (path: string): ReleaseAsset => {
return {
name: basename(path),
mime: mimeOrDefault(path),
size: lstatSync(path).size,
file: readFileSync(path)
};
}
export const mimeOrDefault = (path: string): string => {
return getType(path) || "application/octet-stream";
}
export const upload = async (
gh: GitHub,
url: string,
path: string
): Promise<any> => {
let {
name,
size,
mime,
file
} = asset(path);
console.log(`⬆️ Uploading ${name}...`);
return await gh.repos.uploadReleaseAsset({
url,
headers: {
"content-length": size,
"content-type": mime
},
name,
file
});
}
export const release = async (
config: Config,
gh: GitHub
): Promise<Release> => {
let [owner, repo] = config.github_repository.split("/");
try {
let release = await gh.repos.getReleaseByTag({
owner,
repo,
tag: config.github_ref
});
return release.data;
} catch (error) {
if (error.status === 404) {
const tag_name = config.github_ref.replace("refs/tags/", "");
const name = config.input_name || tag_name;
const body = config.input_body;
const draft = config.input_draft;
let release = await gh.repos.createRelease({
owner,
repo,
tag_name,
name,
body,
draft
});
return release.data;
} else {
throw error;
}
}
}

27
src/main.ts Normal file
View file

@ -0,0 +1,27 @@
import * as fs from 'fs';
import * as core from '@actions/core';
import { GitHub } from '@actions/github';
import { paths, parseConfig, isTag } from './util';
import { release, upload } from './github';
async function run() {
try {
const config = parseConfig(process.env);
if (!isTag(config.github_ref)) {
throw new Error(`⚠️ GitHub Releases requires a tag`);
}
// todo: validate github_ref is a tag
const gh = new GitHub(config.github_token);
let rel = await release(config, gh);
if (config.input_files) {
paths(config.input_files).forEach(async (path) => {
await upload(gh, rel.upload_url, path)
});
}
console.log(`🎉 Release ready at ${rel.html_url}`)
} catch (error) {
core.setFailed(error.message);
}
}
run();

39
src/util.ts Normal file
View file

@ -0,0 +1,39 @@
import * as glob from 'glob';
import { lstatSync } from 'fs';
export interface Config {
github_token: string,
github_ref: string,
github_repository: string,
// user provided
input_name?: string,
input_body?: string,
input_body_path?: string,
input_files?: string[],
input_draft?: boolean,
}
type Env = { [key: string]: string | undefined };
export const parseConfig = (env: Env): Config => {
return {
github_token: env.GITHUB_TOKEN || "",
github_ref: (env.GITHUB_REF || ""),
github_repository: env.GITHUB_REPOSITORY || "",
input_name: env.INPUT_NAME,
input_body: env.INPUT_BODY,
input_body_path: env.INPUT_BODY_PATH,
input_files: (env.INPUT_FILES || "").split(","),
input_draft: env.INPUT_DRAFT === 'true'
}
}
export const paths = (patterns: string[]): string[] => {
return patterns.reduce((acc: string[], pattern: string): string[] => {
return acc.concat(glob.sync(pattern).filter(path => lstatSync(path).isFile()));
}, [])
}
export const isTag = (ref: string): boolean => {
return ref.startsWith("refs/tags/")
}

63
tsconfig.json Normal file
View file

@ -0,0 +1,63 @@
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./lib", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"exclude": ["node_modules", "**/*.test.ts"]
}