safe-push 0.7.4 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/repo-file-sync.yaml +6 -0
- package/.github/workflows/release.yaml +6 -6
- package/.github/workflows/repo-file-sync.yaml +36 -0
- package/dist/index.js +65 -17
- package/package.json +1 -1
- package/renovate.json +22 -0
- package/src/checker.ts +6 -0
- package/src/commands/utils.ts +14 -0
- package/src/git.ts +66 -20
- package/src/types.ts +1 -0
|
@@ -13,7 +13,7 @@ jobs:
|
|
|
13
13
|
should-publish: ${{ steps.check.outputs.should-publish }}
|
|
14
14
|
version: ${{ steps.check.outputs.version }}
|
|
15
15
|
steps:
|
|
16
|
-
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
17
17
|
with:
|
|
18
18
|
fetch-depth: 2
|
|
19
19
|
|
|
@@ -48,10 +48,10 @@ jobs:
|
|
|
48
48
|
contents: write
|
|
49
49
|
id-token: write
|
|
50
50
|
steps:
|
|
51
|
-
- uses: actions/checkout@v4
|
|
51
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
52
52
|
|
|
53
53
|
- name: Setup mise
|
|
54
|
-
uses: jdx/mise-action@v2
|
|
54
|
+
uses: jdx/mise-action@c37c93293d6b742fc901e1406b8f764f6fb19dac # v2.4.4
|
|
55
55
|
|
|
56
56
|
- name: Install tools via mise
|
|
57
57
|
run: mise install
|
|
@@ -63,7 +63,7 @@ jobs:
|
|
|
63
63
|
run: bun run build
|
|
64
64
|
|
|
65
65
|
- name: Setup Node.js
|
|
66
|
-
uses: actions/setup-node@v4
|
|
66
|
+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
67
67
|
with:
|
|
68
68
|
node-version: '20'
|
|
69
69
|
registry-url: 'https://registry.npmjs.org'
|
|
@@ -72,10 +72,10 @@ jobs:
|
|
|
72
72
|
- run: bunx npm@latest publish ./${{ vars.NPM_REGISTRY_NAME }}-${{ needs.check-version.outputs.version }}.tgz --provenance
|
|
73
73
|
|
|
74
74
|
- name: Create GitHub Release
|
|
75
|
-
uses: softprops/action-gh-release@v2
|
|
75
|
+
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
|
76
76
|
with:
|
|
77
77
|
tag_name: v${{ needs.check-version.outputs.version }}
|
|
78
78
|
name: Release v${{ needs.check-version.outputs.version }}
|
|
79
79
|
draft: false
|
|
80
80
|
prerelease: false
|
|
81
|
-
generate_release_notes: true
|
|
81
|
+
generate_release_notes: true
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Sync Files
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: '0 0 * * 0' # Weekly on Sunday
|
|
6
|
+
workflow_dispatch: # Manual trigger
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
sync:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Generate App Token
|
|
13
|
+
id: app-token
|
|
14
|
+
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
|
15
|
+
with:
|
|
16
|
+
app-id: ${{ secrets.GHAPP_REPO_FILE_SYNC_APP_ID }}
|
|
17
|
+
private-key: ${{ secrets.GHAPP_REPO_FILE_SYNC_PRIVATE_KEY }}
|
|
18
|
+
owner: shoppingjaws
|
|
19
|
+
|
|
20
|
+
- name: Checkout
|
|
21
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
22
|
+
with:
|
|
23
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
24
|
+
|
|
25
|
+
- name: Sync Files
|
|
26
|
+
id: sync
|
|
27
|
+
# uses: shoppingjaws/repo-file-sync@e5604c62bd42f3815674beefb2c9cdb8026d8bd8 # 1.0.0
|
|
28
|
+
uses: shoppingjaws/repo-file-sync@main
|
|
29
|
+
with:
|
|
30
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
31
|
+
|
|
32
|
+
- name: Output results
|
|
33
|
+
if: always()
|
|
34
|
+
run: |
|
|
35
|
+
echo "Files synced: ${{ steps.sync.outputs.files-synced }}"
|
|
36
|
+
echo "PR URL: ${{ steps.sync.outputs.pr-url }}"
|
package/dist/index.js
CHANGED
|
@@ -20429,25 +20429,30 @@ async function getLocalEmail() {
|
|
|
20429
20429
|
return execGit(["config", "user.email"]);
|
|
20430
20430
|
});
|
|
20431
20431
|
}
|
|
20432
|
-
async function
|
|
20433
|
-
|
|
20434
|
-
|
|
20435
|
-
|
|
20436
|
-
|
|
20437
|
-
|
|
20432
|
+
async function getBaseBranch(remote = "origin") {
|
|
20433
|
+
const branch = await getCurrentBranch();
|
|
20434
|
+
const isNew = await isNewBranch(remote);
|
|
20435
|
+
if (isNew) {
|
|
20436
|
+
try {
|
|
20437
|
+
await execGit(["rev-parse", "--verify", `${remote}/main`]);
|
|
20438
|
+
return `${remote}/main`;
|
|
20439
|
+
} catch {
|
|
20438
20440
|
try {
|
|
20439
|
-
await execGit(["rev-parse", "--verify", `${remote}/
|
|
20440
|
-
|
|
20441
|
+
await execGit(["rev-parse", "--verify", `${remote}/master`]);
|
|
20442
|
+
return `${remote}/master`;
|
|
20441
20443
|
} catch {
|
|
20442
|
-
|
|
20443
|
-
await execGit(["rev-parse", "--verify", `${remote}/master`]);
|
|
20444
|
-
baseBranch = `${remote}/master`;
|
|
20445
|
-
} catch {
|
|
20446
|
-
return [];
|
|
20447
|
-
}
|
|
20444
|
+
return null;
|
|
20448
20445
|
}
|
|
20449
|
-
}
|
|
20450
|
-
|
|
20446
|
+
}
|
|
20447
|
+
} else {
|
|
20448
|
+
return `${remote}/${branch}`;
|
|
20449
|
+
}
|
|
20450
|
+
}
|
|
20451
|
+
async function getDiffFiles(remote = "origin", authorEmail) {
|
|
20452
|
+
return withSpan("safe-push.git.getDiffFiles", async () => {
|
|
20453
|
+
const baseBranch = await getBaseBranch(remote);
|
|
20454
|
+
if (!baseBranch) {
|
|
20455
|
+
return [];
|
|
20451
20456
|
}
|
|
20452
20457
|
let output;
|
|
20453
20458
|
if (authorEmail) {
|
|
@@ -20472,6 +20477,32 @@ async function getDiffFiles(remote = "origin", authorEmail) {
|
|
|
20472
20477
|
`).filter(Boolean))];
|
|
20473
20478
|
});
|
|
20474
20479
|
}
|
|
20480
|
+
async function getDiffContentForFiles(files, remote = "origin") {
|
|
20481
|
+
return withSpan("safe-push.git.getDiffContentForFiles", async () => {
|
|
20482
|
+
if (files.length === 0) {
|
|
20483
|
+
return {};
|
|
20484
|
+
}
|
|
20485
|
+
const baseBranch = await getBaseBranch(remote);
|
|
20486
|
+
if (!baseBranch) {
|
|
20487
|
+
return {};
|
|
20488
|
+
}
|
|
20489
|
+
const output = await execGit(["diff", `${baseBranch}...HEAD`, "--", ...files]);
|
|
20490
|
+
if (!output) {
|
|
20491
|
+
return {};
|
|
20492
|
+
}
|
|
20493
|
+
const result = {};
|
|
20494
|
+
const sections = output.split(/^(?=diff --git )/m);
|
|
20495
|
+
for (const section of sections) {
|
|
20496
|
+
if (!section.trim())
|
|
20497
|
+
continue;
|
|
20498
|
+
const match = section.match(/^diff --git a\/(.+?) b\//);
|
|
20499
|
+
if (match) {
|
|
20500
|
+
result[match[1]] = section.trim();
|
|
20501
|
+
}
|
|
20502
|
+
}
|
|
20503
|
+
return result;
|
|
20504
|
+
});
|
|
20505
|
+
}
|
|
20475
20506
|
async function execPush(args = [], remote = "origin") {
|
|
20476
20507
|
return withSpan("safe-push.git.execPush", async (span) => {
|
|
20477
20508
|
let pushArgs;
|
|
@@ -20605,11 +20636,13 @@ async function checkPush(config) {
|
|
|
20605
20636
|
const forbiddenFiles = findForbiddenFiles(diffFiles, config.forbiddenPaths);
|
|
20606
20637
|
const hasForbiddenChanges = forbiddenFiles.length > 0;
|
|
20607
20638
|
const isOwnLastCommit = authorEmail.toLowerCase() === localEmail.toLowerCase();
|
|
20639
|
+
const forbiddenDiff = hasForbiddenChanges ? await getDiffContentForFiles(forbiddenFiles) : undefined;
|
|
20608
20640
|
const details = {
|
|
20609
20641
|
isNewBranch: newBranch,
|
|
20610
20642
|
isOwnLastCommit,
|
|
20611
20643
|
hasForbiddenChanges,
|
|
20612
20644
|
forbiddenFiles,
|
|
20645
|
+
forbiddenDiff,
|
|
20613
20646
|
currentBranch,
|
|
20614
20647
|
authorEmail,
|
|
20615
20648
|
localEmail
|
|
@@ -20693,6 +20726,21 @@ function printCheckResultHuman(result) {
|
|
|
20693
20726
|
console.log("Forbidden files changed:");
|
|
20694
20727
|
for (const file of details.forbiddenFiles) {
|
|
20695
20728
|
console.log(` - ${file}`);
|
|
20729
|
+
const diff = details.forbiddenDiff?.[file];
|
|
20730
|
+
if (diff) {
|
|
20731
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
20732
|
+
for (const line of diff.split(`
|
|
20733
|
+
`)) {
|
|
20734
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
20735
|
+
console.log(` \x1B[32m${line}\x1B[0m`);
|
|
20736
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
20737
|
+
console.log(` \x1B[31m${line}\x1B[0m`);
|
|
20738
|
+
} else {
|
|
20739
|
+
console.log(` ${line}`);
|
|
20740
|
+
}
|
|
20741
|
+
}
|
|
20742
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
20743
|
+
}
|
|
20696
20744
|
}
|
|
20697
20745
|
}
|
|
20698
20746
|
console.log("");
|
|
@@ -31307,7 +31355,7 @@ function createMcpCommand() {
|
|
|
31307
31355
|
// package.json
|
|
31308
31356
|
var package_default = {
|
|
31309
31357
|
name: "safe-push",
|
|
31310
|
-
version: "0.7.
|
|
31358
|
+
version: "0.7.5",
|
|
31311
31359
|
description: "Git push safety checker - blocks pushes to forbidden areas",
|
|
31312
31360
|
type: "module",
|
|
31313
31361
|
repository: {
|
package/package.json
CHANGED
package/renovate.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
|
+
"extends": ["config:recommended"],
|
|
4
|
+
"automerge": true,
|
|
5
|
+
"automergeType": "pr",
|
|
6
|
+
"platformAutomerge": false,
|
|
7
|
+
"mise": {
|
|
8
|
+
"enabled": true
|
|
9
|
+
},
|
|
10
|
+
"packageRules": [
|
|
11
|
+
{
|
|
12
|
+
"description": "minor・patchは自動マージ",
|
|
13
|
+
"matchUpdateTypes": ["minor", "patch", "digest", "pin"],
|
|
14
|
+
"automerge": true
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"description": "majorはレビュー必須",
|
|
18
|
+
"matchUpdateTypes": ["major"],
|
|
19
|
+
"automerge": false
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
package/src/checker.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getLastCommitAuthorEmail,
|
|
6
6
|
getLocalEmail,
|
|
7
7
|
getDiffFiles,
|
|
8
|
+
getDiffContentForFiles,
|
|
8
9
|
getRepoVisibility,
|
|
9
10
|
} from "./git";
|
|
10
11
|
import { withSpan } from "./telemetry";
|
|
@@ -108,11 +109,16 @@ export async function checkPush(config: Config): Promise<CheckResult> {
|
|
|
108
109
|
const isOwnLastCommit =
|
|
109
110
|
authorEmail.toLowerCase() === localEmail.toLowerCase();
|
|
110
111
|
|
|
112
|
+
const forbiddenDiff = hasForbiddenChanges
|
|
113
|
+
? await getDiffContentForFiles(forbiddenFiles)
|
|
114
|
+
: undefined;
|
|
115
|
+
|
|
111
116
|
const details = {
|
|
112
117
|
isNewBranch: newBranch,
|
|
113
118
|
isOwnLastCommit,
|
|
114
119
|
hasForbiddenChanges,
|
|
115
120
|
forbiddenFiles,
|
|
121
|
+
forbiddenDiff,
|
|
116
122
|
currentBranch,
|
|
117
123
|
authorEmail,
|
|
118
124
|
localEmail,
|
package/src/commands/utils.ts
CHANGED
|
@@ -72,6 +72,20 @@ export function printCheckResultHuman(result: CheckResult): void {
|
|
|
72
72
|
console.log("Forbidden files changed:");
|
|
73
73
|
for (const file of details.forbiddenFiles) {
|
|
74
74
|
console.log(` - ${file}`);
|
|
75
|
+
const diff = details.forbiddenDiff?.[file];
|
|
76
|
+
if (diff) {
|
|
77
|
+
console.log(" ───────────────────────────────────");
|
|
78
|
+
for (const line of diff.split("\n")) {
|
|
79
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
80
|
+
console.log(` \x1b[32m${line}\x1b[0m`);
|
|
81
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
82
|
+
console.log(` \x1b[31m${line}\x1b[0m`);
|
|
83
|
+
} else {
|
|
84
|
+
console.log(` ${line}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
console.log(" ───────────────────────────────────");
|
|
88
|
+
}
|
|
75
89
|
}
|
|
76
90
|
}
|
|
77
91
|
console.log("");
|
package/src/git.ts
CHANGED
|
@@ -73,6 +73,32 @@ export async function getLocalEmail(): Promise<string> {
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* リモートとの比較基準ブランチを取得
|
|
78
|
+
* 新規ブランチの場合はmainまたはmasterを返す
|
|
79
|
+
* mainもmasterもない場合はnullを返す
|
|
80
|
+
*/
|
|
81
|
+
async function getBaseBranch(remote = "origin"): Promise<string | null> {
|
|
82
|
+
const branch = await getCurrentBranch();
|
|
83
|
+
const isNew = await isNewBranch(remote);
|
|
84
|
+
|
|
85
|
+
if (isNew) {
|
|
86
|
+
try {
|
|
87
|
+
await execGit(["rev-parse", "--verify", `${remote}/main`]);
|
|
88
|
+
return `${remote}/main`;
|
|
89
|
+
} catch {
|
|
90
|
+
try {
|
|
91
|
+
await execGit(["rev-parse", "--verify", `${remote}/master`]);
|
|
92
|
+
return `${remote}/master`;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
return `${remote}/${branch}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
76
102
|
/**
|
|
77
103
|
* リモートとの差分ファイル一覧を取得
|
|
78
104
|
* 新規ブランチの場合はmainまたはmasterとの差分を取得
|
|
@@ -82,26 +108,9 @@ export async function getDiffFiles(
|
|
|
82
108
|
authorEmail?: string
|
|
83
109
|
): Promise<string[]> {
|
|
84
110
|
return withSpan("safe-push.git.getDiffFiles", async () => {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
let baseBranch: string;
|
|
89
|
-
if (isNew) {
|
|
90
|
-
// 新規ブランチの場合、mainまたはmasterを基準にする
|
|
91
|
-
try {
|
|
92
|
-
await execGit(["rev-parse", "--verify", `${remote}/main`]);
|
|
93
|
-
baseBranch = `${remote}/main`;
|
|
94
|
-
} catch {
|
|
95
|
-
try {
|
|
96
|
-
await execGit(["rev-parse", "--verify", `${remote}/master`]);
|
|
97
|
-
baseBranch = `${remote}/master`;
|
|
98
|
-
} catch {
|
|
99
|
-
// mainもmasterもない場合は空の配列を返す
|
|
100
|
-
return [];
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
} else {
|
|
104
|
-
baseBranch = `${remote}/${branch}`;
|
|
111
|
+
const baseBranch = await getBaseBranch(remote);
|
|
112
|
+
if (!baseBranch) {
|
|
113
|
+
return [];
|
|
105
114
|
}
|
|
106
115
|
|
|
107
116
|
let output: string;
|
|
@@ -130,6 +139,43 @@ export async function getDiffFiles(
|
|
|
130
139
|
});
|
|
131
140
|
}
|
|
132
141
|
|
|
142
|
+
/**
|
|
143
|
+
* 指定ファイルごとのdiffコンテンツを取得
|
|
144
|
+
*/
|
|
145
|
+
export async function getDiffContentForFiles(
|
|
146
|
+
files: string[],
|
|
147
|
+
remote = "origin"
|
|
148
|
+
): Promise<Record<string, string>> {
|
|
149
|
+
return withSpan("safe-push.git.getDiffContentForFiles", async () => {
|
|
150
|
+
if (files.length === 0) {
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const baseBranch = await getBaseBranch(remote);
|
|
155
|
+
if (!baseBranch) {
|
|
156
|
+
return {};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const output = await execGit(["diff", `${baseBranch}...HEAD`, "--", ...files]);
|
|
160
|
+
if (!output) {
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// diff 出力を "diff --git" でファイルごとに分割
|
|
165
|
+
const result: Record<string, string> = {};
|
|
166
|
+
const sections = output.split(/^(?=diff --git )/m);
|
|
167
|
+
for (const section of sections) {
|
|
168
|
+
if (!section.trim()) continue;
|
|
169
|
+
// "diff --git a/path b/path" からファイルパスを抽出
|
|
170
|
+
const match = section.match(/^diff --git a\/(.+?) b\//);
|
|
171
|
+
if (match) {
|
|
172
|
+
result[match[1]] = section.trim();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
133
179
|
/**
|
|
134
180
|
* git pushを実行
|
|
135
181
|
*/
|
package/src/types.ts
CHANGED