mirror of
https://github.com/valitydev/repo-file-sync-action.git
synced 2024-11-06 01:45:19 +00:00
🎉 Initial commit
This commit is contained in:
commit
1694b0cec6
1
.eslintrc.js
Normal file
1
.eslintrc.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('@betahuhn/config').eslint
|
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
name: 🐛 Bug report
|
||||
about: Create a report to help us improve
|
||||
labels: bug
|
||||
|
||||
---
|
||||
|
||||
**🐞 Describe the bug**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**📚 To Reproduce**
|
||||
|
||||
Steps to reproduce the behavior.
|
||||
|
||||
**💡 Expected behavior**
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**🖼️ Screenshots**
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**⚙️ Environment**
|
||||
- OS: [e.g. Ubuntu 18.04]
|
||||
- Action version: [e.g. v1.0.0]
|
||||
|
||||
**📋 Additional context**
|
||||
|
||||
Add any other context about the problem here.
|
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 📚 Docs
|
||||
url: https://github.com/BetaHuhn/action-github-file-sync#-usage
|
||||
about: Checkout action-github-file-sync's documentation.
|
||||
- name: 🚀 Feature Request
|
||||
url: https://github.com/BetaHuhn/action-github-file-sync/discussions/new?category=ideas
|
||||
about: Share ideas for new features
|
||||
- name: ❓ Ask a Question
|
||||
url: https://github.com/BetaHuhn/action-github-file-sync/discussions/new?category=q-a
|
||||
about: Ask the community for help
|
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
12
.github/sync.yml
vendored
Normal file
12
.github/sync.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
BetaHuhn/gh-repo-automation:
|
||||
- source: .github/workflows/test.yml
|
||||
dest: .github/workflows/test.yml
|
||||
replace: true
|
||||
- source: .github/dependabot.yml
|
||||
dest: .github/dependabot.yml
|
||||
replace: true
|
||||
|
||||
BetaHuhn/gh-repo-automation:
|
||||
- source: .github/workflows/test.yml
|
||||
dest: .github/workflows/test2.yml
|
||||
replace: true
|
30
.github/workflows/build.yml
vendored
Normal file
30
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
name: Build CI
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
build:
|
||||
name: Build files
|
||||
runs-on: ubuntu-18.04
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Cache node modules
|
||||
uses: c-hive/gha-npm-cache@v1
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build files
|
||||
run: npm run build
|
||||
- name: Commit build
|
||||
uses: stefanzweifel/git-auto-commit-action@v4.8.0
|
||||
with:
|
||||
commit_message: ":rocket: Rebuild files [skip ci]"
|
||||
commit_user_name: BetaHuhn Bot
|
||||
commit_user_email: bot@mxis.ch
|
||||
commit_author: BetaHuhn Bot <bot@mxis.ch>
|
26
.github/workflows/lint.yml
vendored
Normal file
26
.github/workflows/lint.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
name: Lint CI
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- master
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- master
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Cache node modules
|
||||
uses: c-hive/gha-npm-cache@v1
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run lint command
|
||||
run: npm run lint
|
13
.github/workflows/release-scheduler.yml
vendored
Normal file
13
.github/workflows/release-scheduler.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: Release Scheduler CI
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 1"
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
releaseScheduler:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Run release-scheduler
|
||||
uses: koj-co/release-scheduler@master
|
||||
env:
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
32
.github/workflows/release.yml
vendored
Normal file
32
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
name: Release CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
release:
|
||||
name: Build and release
|
||||
runs-on: ubuntu-18.04
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Cache node modules
|
||||
uses: c-hive/gha-npm-cache@v1
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build TypeScript
|
||||
run: npm run build
|
||||
- name: Release
|
||||
run: npx semantic-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GIT_AUTHOR_NAME: "BetaHuhn Bot"
|
||||
GIT_AUTHOR_EMAIL: "bot@mxis.ch"
|
||||
GIT_COMMITTER_NAME: "BetaHuhn Bot"
|
||||
GIT_COMMITTER_EMAIL: "bot@mxis.ch"
|
19
.github/workflows/stale.yml
vendored
Normal file
19
.github/workflows/stale.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
name: "Stale Issues CI"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GH_PAT }}
|
||||
stale-issue-message: "⚠️ This issue has not seen any activity in the past 2 months so I'm marking it as stale. I'll close it if it doesn't see any activity in the coming week."
|
||||
stale-pr-message: "⚠️ This PR has not seen any activity in the past 2 months so I'm marking it as stale. I'll close it if it doesn't see any activity in the coming week."
|
||||
days-before-stale: 60
|
||||
days-before-close: 7
|
||||
stale-issue-label: "wontfix"
|
||||
exempt-issue-labels: "wip"
|
||||
stale-pr-label: "wontfix"
|
||||
exempt-pr-labels: "wip"
|
26
.github/workflows/test.yml
vendored
Normal file
26
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
name: Test CI
|
||||
# test
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- master
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- master
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip-build]')"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Cache node modules
|
||||
uses: c-hive/gha-npm-cache@v1
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run build command
|
||||
run: npm run build
|
61
.gitignore
vendored
Normal file
61
.gitignore
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
node_modules/
|
||||
|
||||
# Editors
|
||||
.vscode/
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Other Dependency directories
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# next.js build output
|
||||
.next
|
0
CHANGELOG.md
Normal file
0
CHANGELOG.md
Normal file
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Maximilian Schiller
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
86
README.md
Normal file
86
README.md
Normal file
@ -0,0 +1,86 @@
|
||||
# action-github-file-sync
|
||||
|
||||
🚧 **UNDER DEVELOPMENT** 🚧
|
||||
|
||||
## Usage
|
||||
|
||||
Use the `sync.yml` file to specify which files should be synced to which repos.
|
||||
|
||||
The top-level key should be used to specify the target repository in the format `username`/`repository-name`@`branch`, after that you can list all the files you want to sync to that individual repository:
|
||||
|
||||
```yml
|
||||
user/repo:
|
||||
- path/to/file.txt
|
||||
user/repo2:
|
||||
- path/to/file2.txt
|
||||
```
|
||||
|
||||
There are multiple ways to specify which files to sync to each individual repository.
|
||||
|
||||
### List individual files
|
||||
|
||||
```yml
|
||||
user/repo:
|
||||
- .github/workflows/build.yml
|
||||
- LICENSE
|
||||
- .gitignore
|
||||
```
|
||||
|
||||
### Different destination path/filename
|
||||
|
||||
```yml
|
||||
user/repo:
|
||||
- source: workflows/build.yml
|
||||
dest: .github/workflows/build.yml
|
||||
- source: LICENSE.md
|
||||
dest: LICENSE
|
||||
```
|
||||
|
||||
### Sync entire directories (coming soon)
|
||||
|
||||
```yml
|
||||
user/repo:
|
||||
- source: workflows/build.yml
|
||||
dest: .github/workflows/build.yml
|
||||
- source: LICENSE.md
|
||||
dest: LICENSE
|
||||
```
|
||||
|
||||
### Match files with regex pattern (coming soon)
|
||||
|
||||
```yml
|
||||
user/repo:
|
||||
- pattern: |
|
||||
^LICENSE$
|
||||
^.github/workflows/sync-.*
|
||||
```
|
||||
|
||||
### Don't replace existing file
|
||||
|
||||
```yml
|
||||
user/repo:
|
||||
- source: .github/workflows/lint.yml
|
||||
replace: false
|
||||
```
|
||||
|
||||
### Don't delete non-existing file (coming soon)
|
||||
|
||||
```yml
|
||||
user/repo:
|
||||
- source: .github/workflows/lint.yml
|
||||
delete: false
|
||||
```
|
||||
|
||||
### Sync the same files to multiple repositories
|
||||
|
||||
```yml
|
||||
group:
|
||||
repos: |
|
||||
user/repo
|
||||
user/repo
|
||||
files:
|
||||
- source: workflows/build.yml
|
||||
dest: .github/workflows/build.yml
|
||||
- source: LICENSE.md
|
||||
dest: LICENSE
|
||||
```
|
52
action.yml
Normal file
52
action.yml
Normal file
@ -0,0 +1,52 @@
|
||||
name: 'GitHub File Sync'
|
||||
description: 'GitHub Action to Sync Files like Workflows Between Repositories.'
|
||||
author: 'BetaHuhn'
|
||||
|
||||
inputs:
|
||||
GH_PAT:
|
||||
description: |
|
||||
GitHub Personal Access Token to use to get repos and write secrets
|
||||
required: false
|
||||
CONFIG_PATH:
|
||||
description: |
|
||||
The path for the sync configuration file
|
||||
required: false
|
||||
PR_LABELS:
|
||||
description: |
|
||||
Labels which will be added to the pull request. Defaults to resync. Set to false to turn off
|
||||
required: false
|
||||
ASSIGNEES:
|
||||
description: |
|
||||
People to assign to the pull request. Defaults to none
|
||||
required: false
|
||||
COMMIT_PREFIX:
|
||||
description: |
|
||||
Prefix for commit message and pull request title. Defaults to 🔄
|
||||
required: false
|
||||
COMMIT_EACH_FILE:
|
||||
description: |
|
||||
Commit each file seperately. Defaults to true
|
||||
required: false
|
||||
GIT_EMAIL:
|
||||
description: |
|
||||
The e-mail address used to commit the synced files. Defaults to the email of the GitHub PAT
|
||||
required: false
|
||||
GIT_USERNAME:
|
||||
description: |
|
||||
The username used to commit the synced files. Defaults to the username of the GitHub PAT
|
||||
required: false
|
||||
TMP_DIR:
|
||||
description: |
|
||||
The working directory where all sync operations will be done. Defaults to `tmp-${Date.now().toString()}`
|
||||
required: false
|
||||
DRY_RUN:
|
||||
description: "Run everything except for nothing will be updated."
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: 'node14'
|
||||
main: 'dist/index.js'
|
||||
|
||||
branding:
|
||||
icon: 'upload-cloud'
|
||||
color: 'gray-dark'
|
30896
dist/index.js
vendored
Normal file
30896
dist/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7638
package-lock.json
generated
Normal file
7638
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
package.json
Normal file
37
package.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "action-github-file-sync",
|
||||
"version": "0.0.1",
|
||||
"description": "GitHub Action to Sync Files like Workflows Between Repositories.",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"lint": "eslint ./src",
|
||||
"build": "ncc build src/index.js -o dist"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/betahuhn/action-github-file-sync.git"
|
||||
},
|
||||
"keywords": [
|
||||
"GitHub",
|
||||
"Actions"
|
||||
],
|
||||
"author": "Maximilian Schiller <schiller@mxis.ch>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/betahuhn/action-github-file-sync/issues"
|
||||
},
|
||||
"homepage": "https://github.com/betahuhn/action-github-file-sync#readme",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.6",
|
||||
"@actions/github": "^2.2.0",
|
||||
"@actions/io": "^1.0.2",
|
||||
"@putout/git-status-porcelain": "^1.1.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"js-yaml": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@betahuhn/config": "^1.0.2",
|
||||
"@vercel/ncc": "^0.26.2",
|
||||
"eslint": "^7.17.0"
|
||||
}
|
||||
}
|
1
release.config.js
Normal file
1
release.config.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('@betahuhn/config').releaseMaster
|
175
src/config.js
Normal file
175
src/config.js
Normal file
@ -0,0 +1,175 @@
|
||||
const core = require('@actions/core')
|
||||
const yaml = require('js-yaml')
|
||||
const fs = require('fs')
|
||||
|
||||
require('dotenv').config()
|
||||
|
||||
const REPLACE_DEFAULT = true
|
||||
const DELETE_DEFAULT = false
|
||||
|
||||
const getVar = ({ key, default: dft, required = false, array = false }) => {
|
||||
const coreVar = core.getInput(key)
|
||||
const envVar = process.env[key]
|
||||
|
||||
if (required === false && (coreVar === false || envVar === 'false'))
|
||||
return undefined
|
||||
|
||||
if (coreVar !== undefined && coreVar.length >= 1)
|
||||
return array ? coreVar.split('\n') : coreVar
|
||||
|
||||
if (envVar !== undefined && envVar.length >= 1)
|
||||
return array ? envVar.split(',') : envVar
|
||||
|
||||
if (required === true)
|
||||
return core.setFailed(`Variable ${ key } missing.`)
|
||||
|
||||
return dft
|
||||
|
||||
}
|
||||
|
||||
const context = {
|
||||
GITHUB_TOKEN: getVar({
|
||||
key: 'GH_PAT',
|
||||
required: true
|
||||
}),
|
||||
GIT_EMAIL: getVar({
|
||||
key: 'GIT_EMAIL'
|
||||
}),
|
||||
GIT_USERNAME: getVar({
|
||||
key: 'GIT_USERNAME'
|
||||
}),
|
||||
CONFIG_PATH: getVar({
|
||||
key: 'CONFIG_PATH',
|
||||
default: '.github/sync.yml'
|
||||
}),
|
||||
COMMIT_PREFIX: getVar({
|
||||
key: 'COMMIT_PREFIX',
|
||||
default: '🔄'
|
||||
}),
|
||||
COMMIT_EACH_FILE: getVar({
|
||||
key: 'COMMIT_EACH_FILE',
|
||||
default: true
|
||||
}),
|
||||
PR_LABELS: getVar({
|
||||
key: 'PR_LABELS',
|
||||
default: [ 'resync' ],
|
||||
required: false,
|
||||
array: true
|
||||
}),
|
||||
ASSIGNEES: getVar({
|
||||
key: 'ASSIGNEES',
|
||||
required: false,
|
||||
array: true
|
||||
}),
|
||||
TMP_DIR: getVar({
|
||||
key: 'TMP_DIR',
|
||||
default: `tmp-${ Date.now().toString() }`
|
||||
}),
|
||||
DRY_RUN: getVar({
|
||||
key: 'DRY_RUN',
|
||||
default: false
|
||||
}),
|
||||
GITHUB_REPOSITORY: getVar({
|
||||
key: 'GITHUB_REPOSITORY',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
|
||||
core.setSecret(context.GITHUB_TOKEN)
|
||||
|
||||
core.debug(
|
||||
JSON.stringify(
|
||||
context,
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
const parseRepoName = (fullRepo) => {
|
||||
const user = fullRepo.split('/')[0]
|
||||
const name = fullRepo.split('/')[1].split('@')[0]
|
||||
const branch = fullRepo.split('/')[1].split('@')[1] || 'default'
|
||||
|
||||
return {
|
||||
fullName: `${ user }/${ name }`,
|
||||
user,
|
||||
name,
|
||||
branch
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const parseFiles = (files) => {
|
||||
return files.map((item) => {
|
||||
|
||||
if (typeof item === 'string') {
|
||||
return {
|
||||
source: item,
|
||||
dest: item,
|
||||
replace: REPLACE_DEFAULT,
|
||||
delete: DELETE_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
if (item.source !== undefined) {
|
||||
return {
|
||||
source: item.source,
|
||||
dest: item.dest !== undefined ? item.dest : item.source,
|
||||
replace: item.replace !== undefined ? item.replace : REPLACE_DEFAULT,
|
||||
delete: item.delete !== undefined ? item.delete : DELETE_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
if (item.pattern !== undefined) {
|
||||
return {
|
||||
pattern: item.pattern,
|
||||
dest: item.dest,
|
||||
replace: item.replace !== undefined ? item.replace : REPLACE_DEFAULT,
|
||||
delete: item.delete !== undefined ? item.delete : DELETE_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
core.wanr('Warn: No source files specified')
|
||||
})
|
||||
}
|
||||
|
||||
const parseConfig = async () => {
|
||||
const fileContent = await fs.promises.readFile(context.CONFIG_PATH)
|
||||
|
||||
const configObject = yaml.load(fileContent.toString())
|
||||
|
||||
const result = []
|
||||
|
||||
Object.keys(configObject).forEach((key) => {
|
||||
if (key === 'group') {
|
||||
const object = configObject[key]
|
||||
const repos = typeof object.repos === 'string' ? object.repos.split('\n').filter((n) => n) : object.repos
|
||||
|
||||
repos.forEach((name) => {
|
||||
const files = parseFiles(object.files)
|
||||
result.push({
|
||||
repo: parseRepoName(name),
|
||||
files
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const files = parseFiles(configObject[key])
|
||||
result.push({
|
||||
repo: parseRepoName(key),
|
||||
files
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
while (fs.existsSync(context.TMP_DIR)) {
|
||||
context.TMP_DIR = `tmp-${ Date.now().toString() }`
|
||||
core.warn(`TEMP_DIR already exists. Using "${ context.TMP_DIR }" now.`)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
...context,
|
||||
parseConfig
|
||||
}
|
139
src/git.js
Normal file
139
src/git.js
Normal file
@ -0,0 +1,139 @@
|
||||
const { exec } = require('child_process')
|
||||
const { parse } = require('@putout/git-status-porcelain')
|
||||
const core = require('@actions/core')
|
||||
const path = require('path')
|
||||
|
||||
const {
|
||||
GITHUB_TOKEN,
|
||||
GIT_USERNAME,
|
||||
GIT_EMAIL,
|
||||
TMP_DIR,
|
||||
COMMIT_PREFIX,
|
||||
GITHUB_REPOSITORY
|
||||
} = require('./config')
|
||||
|
||||
const init = (repo) => {
|
||||
|
||||
const localPath = path.join(TMP_DIR, repo.fullName)
|
||||
const gitUrl = `https://${ GITHUB_TOKEN }@github.com/${ repo.fullName }.git`
|
||||
|
||||
const clone = () => {
|
||||
core.info(`Cloning ${ repo.fullName } into ${ localPath }`)
|
||||
|
||||
return execCmd(
|
||||
`git clone --depth 1 ${ repo.branch !== 'default' ? '--branch "' + repo.branch + '"' : '' } ${ gitUrl } ${ localPath }`
|
||||
)
|
||||
}
|
||||
|
||||
const setIdentity = async (client) => {
|
||||
let username = GIT_USERNAME
|
||||
let email = GIT_EMAIL
|
||||
|
||||
if (email === undefined) {
|
||||
const { data } = await client.users.getAuthenticated()
|
||||
email = data.email
|
||||
username = data.login
|
||||
}
|
||||
|
||||
core.info(`Setting git user to email: ${ email }, username: ${ username }`)
|
||||
|
||||
return execCmd(
|
||||
`git config --local user.name "${ username }" && git config --local user.email "${ email }"`,
|
||||
localPath
|
||||
)
|
||||
}
|
||||
|
||||
const currentBranch = async () => {
|
||||
return execCmd(
|
||||
`git rev-parse --abbrev-ref HEAD`,
|
||||
localPath
|
||||
)
|
||||
}
|
||||
|
||||
const createPrBranch = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timestamp = Math.round((new Date()).getTime() / 1000)
|
||||
const newBranch = `file-sync/${ repo.branch }-${ timestamp }`
|
||||
|
||||
core.info(`Creating PR Branch ${ newBranch }`)
|
||||
|
||||
execCmd(
|
||||
`git checkout -b "${ newBranch }"`,
|
||||
localPath
|
||||
).catch((err) => {
|
||||
reject(err)
|
||||
}).then(() => {
|
||||
resolve(newBranch)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const add = async (file) => {
|
||||
return execCmd(
|
||||
`git add -f ${ file }`,
|
||||
localPath
|
||||
)
|
||||
}
|
||||
|
||||
const hasChange = async () => {
|
||||
const statusOutput = await execCmd(
|
||||
`git status --porcelain`,
|
||||
localPath
|
||||
)
|
||||
return parse(statusOutput).length !== 0
|
||||
}
|
||||
|
||||
const commit = async (dest, source) => {
|
||||
const message = dest !== undefined ? `${ COMMIT_PREFIX } Resynced '${ dest }' with '${ GITHUB_REPOSITORY }/${ source }'` : `${ COMMIT_PREFIX } Resynced file(s) with ${ GITHUB_REPOSITORY }`
|
||||
return execCmd(
|
||||
`git commit -m "${ message }"`,
|
||||
localPath
|
||||
)
|
||||
}
|
||||
|
||||
const status = async () => {
|
||||
return execCmd(
|
||||
`git status`,
|
||||
localPath
|
||||
)
|
||||
}
|
||||
|
||||
const push = async () => {
|
||||
return execCmd(
|
||||
`git push ${ gitUrl }`,
|
||||
localPath
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
localPath,
|
||||
clone,
|
||||
setIdentity,
|
||||
createPrBranch,
|
||||
add,
|
||||
hasChange,
|
||||
commit,
|
||||
status,
|
||||
push,
|
||||
currentBranch
|
||||
}
|
||||
}
|
||||
|
||||
const execCmd = (command, workingDir) => {
|
||||
core.debug(`EXEC: "${ command }" IN ${ workingDir }`)
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(
|
||||
command,
|
||||
{
|
||||
cwd: workingDir
|
||||
},
|
||||
function(error, stdout) {
|
||||
error ? reject(error) : resolve(stdout.trim())
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init
|
||||
}
|
39
src/helpers.js
Normal file
39
src/helpers.js
Normal file
@ -0,0 +1,39 @@
|
||||
// From https://github.com/toniov/p-iteration/blob/master/lib/static-methods.js - MIT © Antonio V
|
||||
const forEach = async (array, callback) => {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
// eslint-disable-next-line callback-return
|
||||
await callback(array[index], index, array)
|
||||
}
|
||||
}
|
||||
|
||||
// From https://github.com/MartinKolarik/dedent-js/blob/master/src/index.ts - Copyright (c) 2015 Martin Kolárik. Released under the MIT license.
|
||||
const dedent = function(templateStrings, ...values) {
|
||||
const matches = []
|
||||
const strings = typeof templateStrings === 'string' ? [ templateStrings ] : templateStrings.slice()
|
||||
strings[strings.length - 1] = strings[strings.length - 1].replace(/\r?\n([\t ]*)$/, '')
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
let match
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
if (match = strings[i].match(/\n[\t ]+/g)) {
|
||||
matches.push(...match)
|
||||
}
|
||||
}
|
||||
if (matches.length) {
|
||||
const size = Math.min(...matches.map((value) => value.length - 1))
|
||||
const pattern = new RegExp(`\n[\t ]{${ size }}`, 'g')
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
strings[i] = strings[i].replace(pattern, '\n')
|
||||
}
|
||||
}
|
||||
strings[0] = strings[0].replace(/^\r?\n/, '')
|
||||
let string = strings[0]
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
string += values[i] + strings[i + 1]
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
forEach,
|
||||
dedent
|
||||
}
|
201
src/index.js
Normal file
201
src/index.js
Normal file
@ -0,0 +1,201 @@
|
||||
const core = require('@actions/core')
|
||||
const github = require('@actions/github')
|
||||
const io = require('@actions/io')
|
||||
const fs = require('fs')
|
||||
|
||||
const Git = require('./git')
|
||||
const { forEach, dedent } = require('./helpers')
|
||||
|
||||
const {
|
||||
parseConfig,
|
||||
GITHUB_TOKEN,
|
||||
COMMIT_EACH_FILE,
|
||||
GITHUB_REPOSITORY,
|
||||
COMMIT_PREFIX,
|
||||
PR_LABELS,
|
||||
ASSIGNEES,
|
||||
DRY_RUN
|
||||
} = require('./config')
|
||||
|
||||
const run = async () => {
|
||||
const client = new github.GitHub(GITHUB_TOKEN)
|
||||
|
||||
const repos = await parseConfig()
|
||||
|
||||
await forEach(repos, async (item) => {
|
||||
core.info(`Repository Info`)
|
||||
core.info(`Slug : ${ item.repo.name }`)
|
||||
core.info(`Owner : ${ item.repo.user }`)
|
||||
core.info(`Https Url : https://github.com/${ item.repo.fullName }`)
|
||||
core.info(`Branch : ${ item.repo.branch }`)
|
||||
core.info(' ')
|
||||
try {
|
||||
const git = Git.init(item.repo)
|
||||
|
||||
await git.clone()
|
||||
await git.setIdentity(client)
|
||||
|
||||
const currentBranch = await git.currentBranch()
|
||||
const prBranch = await git.createPrBranch()
|
||||
|
||||
const modified = []
|
||||
|
||||
await forEach(item.files, async (file) => {
|
||||
if (file.pattern !== undefined) {
|
||||
core.warning('Pattern not supported yet')
|
||||
return
|
||||
}
|
||||
|
||||
const fileExists = fs.existsSync(file.source)
|
||||
if (fileExists === false) {
|
||||
core.warning(`Source file ${ file.source } not found`)
|
||||
return
|
||||
}
|
||||
|
||||
const stat = await fs.promises.lstat(file.source)
|
||||
const isFile = stat.isFile()
|
||||
if (isFile === false) {
|
||||
core.warning(`Directory as source not supported yet`)
|
||||
// io.cp(file.source, dest, { recursive: false, force: false })
|
||||
return
|
||||
}
|
||||
|
||||
const dest = `${ git.localPath }/${ file.dest }`
|
||||
const destExists = fs.existsSync(dest)
|
||||
if (destExists === true && file.replace === false) {
|
||||
core.warning(`File already exists in destination and 'replace' option is set to false`)
|
||||
return
|
||||
}
|
||||
|
||||
core.info(`Copying ${ file.source } to ${ dest }`)
|
||||
await io.cp(file.source, dest, { recursive: false, force: true }).catch((err) => {
|
||||
core.error(`Unable to copy file.`)
|
||||
core.error(err)
|
||||
}).then(async () => {
|
||||
await git.add(file.dest)
|
||||
|
||||
if (COMMIT_EACH_FILE === true) {
|
||||
const hasChange = await git.hasChange()
|
||||
if (hasChange === false) {
|
||||
core.info('File already up to date')
|
||||
return
|
||||
}
|
||||
|
||||
core.info(`Creating commit for file ${ file.dest }`)
|
||||
await git.commit(file.dest, file.source)
|
||||
modified.push({
|
||||
dest: file.dest,
|
||||
source: file.source
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (DRY_RUN) {
|
||||
core.warning('Dry run, no changes will be pushed')
|
||||
core.info('Git Status')
|
||||
core.info(await git.status())
|
||||
return
|
||||
}
|
||||
|
||||
const hasChange = await git.hasChange()
|
||||
if (hasChange === false && COMMIT_EACH_FILE === false) {
|
||||
core.info('File(s) already up to date')
|
||||
return
|
||||
}
|
||||
|
||||
if (hasChange === true) {
|
||||
core.info(`Creating commit for remaining files`)
|
||||
await git.commit()
|
||||
modified.push({
|
||||
dest: git.localPath
|
||||
})
|
||||
}
|
||||
|
||||
if (modified.length < 1) {
|
||||
core.info('Nothing to push')
|
||||
return
|
||||
}
|
||||
|
||||
core.info(`Pushing changes to remote`)
|
||||
await git.push()
|
||||
|
||||
let changedFiles = ''
|
||||
let list = ``
|
||||
|
||||
if (COMMIT_EACH_FILE === true) {
|
||||
modified.forEach((file) => {
|
||||
list += `<li><code>${ file.dest }</code> with <code>${ file.source }</code></li>`
|
||||
})
|
||||
|
||||
changedFiles = dedent(`
|
||||
<details>
|
||||
<summary>Synced files</summary>
|
||||
<ul>
|
||||
${ list }
|
||||
</ul>
|
||||
</details>
|
||||
`)
|
||||
}
|
||||
|
||||
|
||||
core.info(`Creating new PR`)
|
||||
const { data } = await client.pulls.create({
|
||||
owner: item.repo.user,
|
||||
repo: item.repo.name,
|
||||
title: `${ COMMIT_PREFIX } Resynced file(s) with ${ GITHUB_REPOSITORY }`,
|
||||
body: dedent(`
|
||||
Resynced file(s) with [${ GITHUB_REPOSITORY }](https://github.com/${ GITHUB_REPOSITORY }).
|
||||
|
||||
${ changedFiles }
|
||||
|
||||
---
|
||||
|
||||
This PR was created automatically by the [action-github-file-sync](https://github.com/BetaHuhn/action-github-file-sync) workflow run [#${ process.env.GITHUB_RUN_NUMBER || 0 }](https://github.com/${ GITHUB_REPOSITORY }/actions/runs/${ process.env.GITHUB_RUN_NUMBER || 0 })
|
||||
`),
|
||||
head: prBranch,
|
||||
base: currentBranch
|
||||
})
|
||||
|
||||
core.info(`Pull Request Created: #${ data.number }`)
|
||||
core.info(`${ data.html_url }`)
|
||||
|
||||
core.setOutput('pull_request_number', data.number)
|
||||
core.setOutput('pull_request_url', data.html_url)
|
||||
|
||||
if (PR_LABELS !== undefined && PR_LABELS.length > 0) {
|
||||
core.info(`Adding label(s) "${ PR_LABELS.join(', ') }" to PR`)
|
||||
await client.issues.addLabels({
|
||||
owner: item.repo.user,
|
||||
repo: item.repo.name,
|
||||
issue_number: data.number,
|
||||
labels: PR_LABELS
|
||||
})
|
||||
}
|
||||
|
||||
if (ASSIGNEES !== undefined && ASSIGNEES.length > 0) {
|
||||
core.info(`Adding assignee(s) "${ ASSIGNEES.join(', ') }" to PR`)
|
||||
await client.issues.addAssignees({
|
||||
owner: item.repo.user,
|
||||
repo: item.repo.name,
|
||||
issue_number: data.number,
|
||||
assignees: ASSIGNEES
|
||||
})
|
||||
}
|
||||
|
||||
core.info(' ')
|
||||
} catch (err) {
|
||||
core.error(err.message)
|
||||
core.error(err)
|
||||
}
|
||||
})
|
||||
|
||||
core.info('DONE')
|
||||
}
|
||||
|
||||
run()
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
console.error('ERROR', err)
|
||||
core.setFailed(err.message)
|
||||
})
|
Loading…
Reference in New Issue
Block a user