mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
fix(ci): harden self-hosted workflow trust boundaries
Keep active CCS workflows on self-hosted runners, gate self-hosted PR execution to trusted authors, and scope privileged release credentials to the exact git operations that need them.
This commit is contained in:
@@ -74,10 +74,14 @@ jobs:
|
||||
if: >-
|
||||
github.event_name != 'workflow_dispatch' &&
|
||||
(
|
||||
github.event_name == 'pull_request_target' ||
|
||||
(
|
||||
github.event_name == 'pull_request_target' &&
|
||||
contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.pull_request.author_association)
|
||||
) ||
|
||||
(
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.issue.author_association) &&
|
||||
github.event.comment.body == '/review' &&
|
||||
(
|
||||
contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.comment.author_association) ||
|
||||
|
||||
@@ -16,6 +16,8 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
if: >-
|
||||
contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.pull_request.author_association)
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
env:
|
||||
# Keep Bun cache isolated per job workspace so parallel self-hosted runs
|
||||
@@ -58,6 +60,8 @@ jobs:
|
||||
run: ${{ matrix.check.cmd }}
|
||||
|
||||
build:
|
||||
if: >-
|
||||
contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.pull_request.author_association)
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
name: build
|
||||
env:
|
||||
@@ -98,6 +102,8 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
test:
|
||||
if: >-
|
||||
contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.pull_request.author_association)
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
name: test
|
||||
needs: [build]
|
||||
|
||||
@@ -21,7 +21,10 @@ on:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event_name != 'pull_request' ||
|
||||
contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.pull_request.author_association)
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
name: Deploy ccs-installer to CloudFlare
|
||||
|
||||
steps:
|
||||
|
||||
@@ -50,9 +50,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: dev
|
||||
# PAT_TOKEN required: dev-release pushes version bump commits to dev,
|
||||
# and protected branches reject github.token writes once status checks are required.
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
@@ -93,8 +91,16 @@ jobs:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
# Use custom dev release script instead of semantic-release
|
||||
# This ensures dev versions follow {stable}-dev.{N} pattern
|
||||
auth_header=$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')
|
||||
echo "::add-mask::${auth_header}"
|
||||
export GIT_CONFIG_COUNT=2
|
||||
export GIT_CONFIG_KEY_0=http.https://github.com/kaitranntt/ccs.extraheader
|
||||
export GIT_CONFIG_VALUE_0="AUTHORIZATION: basic ${auth_header}"
|
||||
export GIT_CONFIG_KEY_1=http.https://github.com/kaitranntt/ccs.git.extraheader
|
||||
export GIT_CONFIG_VALUE_1="AUTHORIZATION: basic ${auth_header}"
|
||||
|
||||
# Use custom dev release script instead of semantic-release.
|
||||
# This ensures dev versions follow {stable}-dev.{N} pattern.
|
||||
chmod +x scripts/dev-release.sh
|
||||
./scripts/dev-release.sh
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ concurrency:
|
||||
jobs:
|
||||
publish:
|
||||
if: ${{ github.event_name != 'release' || !github.event.release.prerelease }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: read
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
|
||||
permissions:
|
||||
contents: write # For creating GitHub releases
|
||||
@@ -66,4 +66,4 @@ jobs:
|
||||
## What's Changed
|
||||
See [CHANGELOG.md](https://github.com/kaitranntt/ccs/blob/main/CHANGELOG.md)
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -21,9 +21,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# PAT_TOKEN required: semantic-release pushes version bump commits to main,
|
||||
# which needs branch protection bypass. github.token cannot push to protected branches.
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -58,12 +56,20 @@ jobs:
|
||||
env:
|
||||
HUSKY: 0
|
||||
# PAT_TOKEN bypasses branch protection so semantic-release can push
|
||||
# version bump + CHANGELOG commits directly to main
|
||||
# version bump + CHANGELOG commits directly to main.
|
||||
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.PAT_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
auth_header=$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')
|
||||
echo "::add-mask::${auth_header}"
|
||||
export GIT_CONFIG_COUNT=2
|
||||
export GIT_CONFIG_KEY_0=http.https://github.com/kaitranntt/ccs.extraheader
|
||||
export GIT_CONFIG_VALUE_0="AUTHORIZATION: basic ${auth_header}"
|
||||
export GIT_CONFIG_KEY_1=http.https://github.com/kaitranntt/ccs.git.extraheader
|
||||
export GIT_CONFIG_VALUE_1="AUTHORIZATION: basic ${auth_header}"
|
||||
|
||||
set +e
|
||||
OUTPUT=$(bun x semantic-release 2>&1)
|
||||
STATUS=$?
|
||||
|
||||
@@ -22,7 +22,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
sync-project:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -10,16 +10,20 @@ jobs:
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# PAT_TOKEN is required because this pushes to protected dev and must
|
||||
# trigger follow-up Push CI / Dev Release workflows.
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Harden Git workspace
|
||||
run: |
|
||||
rm -rf .git/hooks
|
||||
mkdir -p .git/hooks
|
||||
git config --local core.hooksPath /dev/null
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
@@ -47,4 +51,14 @@ jobs:
|
||||
git commit --no-edit
|
||||
fi
|
||||
|
||||
git push origin dev
|
||||
# PAT_TOKEN is required because this pushes to protected dev and must
|
||||
# trigger follow-up Push CI / Dev Release workflows. Keep it scoped to
|
||||
# the final push rather than persisting it in the checkout config.
|
||||
auth_header=$(printf 'x-access-token:%s' "$PAT_TOKEN" | base64 | tr -d '\n')
|
||||
echo "::add-mask::${auth_header}"
|
||||
git -c core.hooksPath=/dev/null \
|
||||
-c "http.https://github.com/kaitranntt/ccs.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
||||
-c "http.https://github.com/kaitranntt/ccs.git.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
||||
push origin dev
|
||||
env:
|
||||
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
|
||||
|
||||
@@ -50,6 +50,13 @@ AI MUST NOT declare a task done, close a session, or move to the next task while
|
||||
- Verified on `2026-04-22` via `gh api repos/kaitranntt/ccs/branches/dev/protection`: `dev` currently requires `typecheck`, `lint`, `format`, `build`, and `test`, has no branch restrictions, and has no required PR-review gate.
|
||||
- `dev-release.yml` currently pushes with `PAT_TOKEN` because `dev` is protected by those required status checks. Do not switch it back to `github.token` unless branch protection changes with it.
|
||||
|
||||
### Self-Hosted Runner Policy
|
||||
|
||||
- Keep active CCS workflows on local self-hosted runners by default. Do **not** move jobs to `ubuntu-latest`, `macos-latest`, or other GitHub-hosted runners as the first security fix.
|
||||
- For this public repo, standard GitHub-hosted runners may not consume billable minutes, but the project policy is still local-runner-first to keep compute predictable and avoid future quota surprises across repos.
|
||||
- Make self-hosted use secure by gating untrusted PR triggers, avoiding checkout of untrusted code in privileged `pull_request_target` jobs, setting `persist-credentials: false`, and scoping PAT-backed credentials to the exact release/sync push step.
|
||||
- If a workflow truly needs GitHub-hosted runners, document the exception in this file and add a regression test for the exception.
|
||||
|
||||
## Core Function
|
||||
|
||||
Multi-provider profile and runtime manager for Claude Code, Factory Droid,
|
||||
|
||||
@@ -7,7 +7,7 @@ function resolvePath(relativePath: string) {
|
||||
}
|
||||
|
||||
describe('PR-Agent review lane migration', () => {
|
||||
test('keeps ai-review.yml as the PR-Agent workflow on the self-hosted cliproxy runner', () => {
|
||||
test('keeps ai-review.yml as the PR-Agent workflow on the self-hosted cliproxy runner with trusted PR gating', () => {
|
||||
const workflowPath = resolvePath('../../../../.github/workflows/ai-review.yml');
|
||||
const prAgentConfigPath = resolvePath('../../../../.pr_agent.toml');
|
||||
|
||||
@@ -23,6 +23,14 @@ describe('PR-Agent review lane migration', () => {
|
||||
expect(workflow).toContain('uses: qodo-ai/pr-agent');
|
||||
expect(appTokenUsages).toHaveLength(2);
|
||||
expect(workflow).toContain('OPENAI.API_BASE');
|
||||
expect(workflow).toContain("github.event_name == 'pull_request_target' &&");
|
||||
expect(workflow).toContain(
|
||||
'contains(fromJSON(\'["COLLABORATOR","MEMBER","OWNER"]\'), github.event.pull_request.author_association)'
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
'contains(fromJSON(\'["COLLABORATOR","MEMBER","OWNER"]\'), github.event.issue.author_association)'
|
||||
);
|
||||
expect(workflow).toContain('github.event.issue.author_association');
|
||||
expect(workflow).toContain('OPENAI_KEY');
|
||||
expect(workflow).toContain('vars.AI_REVIEW_BASE_URL');
|
||||
expect(workflow).toContain('vars.AI_REVIEW_MODEL');
|
||||
|
||||
@@ -9,6 +9,8 @@ function resolvePath(relativePath: string) {
|
||||
describe('pr ci workflow', () => {
|
||||
test('keeps full coverage on pull requests', () => {
|
||||
const workflowPath = resolvePath('../../../../.github/workflows/ci.yml');
|
||||
const trustedAuthorGate =
|
||||
'contains(fromJSON(\'["COLLABORATOR","MEMBER","OWNER"]\'), github.event.pull_request.author_association)';
|
||||
|
||||
expect(fs.existsSync(workflowPath)).toBe(true);
|
||||
|
||||
@@ -17,6 +19,7 @@ describe('pr ci workflow', () => {
|
||||
expect(workflow).toContain('name: CI');
|
||||
expect(workflow).toContain('pull_request:');
|
||||
expect(workflow).toContain('branches: [main, dev]');
|
||||
expect(workflow.split(trustedAuthorGate).length - 1).toBe(3);
|
||||
expect(workflow).toContain('group: ci-${{ github.ref }}');
|
||||
expect(workflow).toContain('cancel-in-progress: true');
|
||||
expect(workflow).toContain('fail-fast: false');
|
||||
|
||||
@@ -26,8 +26,15 @@ describe('dev release workflow', () => {
|
||||
expect(workflow).toContain('branches: [dev]');
|
||||
expect(workflow.match(/runs-on: \[self-hosted, linux, x64\]/g)).toHaveLength(2);
|
||||
expect(workflow).not.toContain('runs-on: ubuntu-latest');
|
||||
expect(checkoutSection).toContain("token: ${{ secrets.PAT_TOKEN }}");
|
||||
expect(checkoutSection).toContain('persist-credentials: false');
|
||||
expect(checkoutSection).not.toContain('token: ${{ secrets.PAT_TOKEN }}');
|
||||
expect(checkoutSection).not.toContain('token: ${{ github.token }}');
|
||||
expect(releaseSection).toContain('echo "::add-mask::${auth_header}"');
|
||||
expect(releaseSection).toContain('GIT_CONFIG_COUNT=2');
|
||||
expect(releaseSection).toContain('GIT_CONFIG_KEY_0=http.https://github.com/kaitranntt/ccs.extraheader');
|
||||
expect(releaseSection).toContain('GIT_CONFIG_VALUE_0="AUTHORIZATION: basic ${auth_header}"');
|
||||
expect(releaseSection).toContain('GIT_CONFIG_KEY_1=http.https://github.com/kaitranntt/ccs.git.extraheader');
|
||||
expect(releaseSection).toContain('GIT_CONFIG_VALUE_1="AUTHORIZATION: basic ${auth_header}"');
|
||||
expect(releaseSection).toContain('GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}');
|
||||
expect(releaseSection).toContain('GH_TOKEN: ${{ secrets.PAT_TOKEN }}');
|
||||
expect(releaseSection).not.toContain('GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}');
|
||||
|
||||
@@ -7,7 +7,7 @@ function resolvePath(relativePath: string) {
|
||||
}
|
||||
|
||||
describe('stable release workflow', () => {
|
||||
test('uses the protected-branch runner and release token path', () => {
|
||||
test('uses the self-hosted runner and scoped release token path', () => {
|
||||
const workflowPath = resolvePath('../../../../.github/workflows/release.yml');
|
||||
|
||||
expect(fs.existsSync(workflowPath)).toBe(true);
|
||||
@@ -26,7 +26,14 @@ describe('stable release workflow', () => {
|
||||
expect(workflow).toContain('branches: [main]');
|
||||
expect(workflow).toContain('runs-on: [self-hosted, linux, x64]');
|
||||
expect(workflow).not.toContain('runs-on: ubuntu-latest');
|
||||
expect(checkoutSection).toContain('token: ${{ secrets.PAT_TOKEN }}');
|
||||
expect(checkoutSection).toContain('persist-credentials: false');
|
||||
expect(checkoutSection).not.toContain('token: ${{ secrets.PAT_TOKEN }}');
|
||||
expect(releaseSection).toContain('echo "::add-mask::${auth_header}"');
|
||||
expect(releaseSection).toContain('GIT_CONFIG_COUNT=2');
|
||||
expect(releaseSection).toContain('GIT_CONFIG_KEY_0=http.https://github.com/kaitranntt/ccs.extraheader');
|
||||
expect(releaseSection).toContain('GIT_CONFIG_VALUE_0="AUTHORIZATION: basic ${auth_header}"');
|
||||
expect(releaseSection).toContain('GIT_CONFIG_KEY_1=http.https://github.com/kaitranntt/ccs.git.extraheader');
|
||||
expect(releaseSection).toContain('GIT_CONFIG_VALUE_1="AUTHORIZATION: basic ${auth_header}"');
|
||||
expect(releaseSection).toContain('GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}');
|
||||
expect(releaseSection).toContain('GH_TOKEN: ${{ secrets.PAT_TOKEN }}');
|
||||
expect(releaseSection).toContain('NPM_TOKEN: ${{ secrets.NPM_TOKEN }}');
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
function workflowsDir() {
|
||||
return path.resolve(import.meta.dir, '../../../../.github/workflows');
|
||||
}
|
||||
|
||||
describe('self-hosted runner policy', () => {
|
||||
test('keeps active workflows on local runners', () => {
|
||||
const hostedRunnerLabels = [
|
||||
'ubuntu-latest',
|
||||
'ubuntu-24.04',
|
||||
'ubuntu-22.04',
|
||||
'macos-latest',
|
||||
'windows-latest',
|
||||
];
|
||||
const workflowFiles = fs
|
||||
.readdirSync(workflowsDir())
|
||||
.filter((file) => file.endsWith('.yml') || file.endsWith('.yaml'));
|
||||
|
||||
expect(workflowFiles.length).toBeGreaterThan(0);
|
||||
|
||||
for (const file of workflowFiles) {
|
||||
const workflow = fs.readFileSync(path.join(workflowsDir(), file), 'utf8');
|
||||
|
||||
for (const label of hostedRunnerLabels) {
|
||||
expect(workflow, `${file} must not use GitHub-hosted runner ${label}`).not.toContain(
|
||||
`runs-on: ${label}`
|
||||
);
|
||||
}
|
||||
|
||||
expect(workflow, `${file} must target a self-hosted runner`).toContain('self-hosted');
|
||||
}
|
||||
});
|
||||
|
||||
test('gates pull-request self-hosted worker deploys to trusted authors', () => {
|
||||
const workflow = fs.readFileSync(path.join(workflowsDir(), 'deploy-ccs-worker.yml'), 'utf8');
|
||||
|
||||
expect(workflow).toContain("github.event_name != 'pull_request'");
|
||||
expect(workflow).toContain(
|
||||
'contains(fromJSON(\'["COLLABORATOR","MEMBER","OWNER"]\'), github.event.pull_request.author_association)'
|
||||
);
|
||||
});
|
||||
|
||||
test('gates pull-request workflows that check out code on self-hosted runners', () => {
|
||||
const trustedAuthorGate =
|
||||
'contains(fromJSON(\'["COLLABORATOR","MEMBER","OWNER"]\'), github.event.pull_request.author_association)';
|
||||
const workflowFiles = fs
|
||||
.readdirSync(workflowsDir())
|
||||
.filter((file) => file.endsWith('.yml') || file.endsWith('.yaml'));
|
||||
|
||||
for (const file of workflowFiles) {
|
||||
const workflow = fs.readFileSync(path.join(workflowsDir(), file), 'utf8');
|
||||
|
||||
if (workflow.includes('pull_request_target:')) {
|
||||
expect(workflow, `${file} must not check out code from pull_request_target`).not.toContain(
|
||||
'uses: actions/checkout'
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
workflow.includes('pull_request:') &&
|
||||
workflow.includes('self-hosted') &&
|
||||
workflow.includes('uses: actions/checkout')
|
||||
) {
|
||||
expect(workflow, `${file} must gate self-hosted PR checkout to trusted authors`).toContain(
|
||||
trustedAuthorGate
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('scoped PAT headers match repository URL forms used by git', () => {
|
||||
const header = 'AUTHORIZATION: basic test-token';
|
||||
const env = {
|
||||
...process.env,
|
||||
GIT_CONFIG_COUNT: '2',
|
||||
GIT_CONFIG_KEY_0: 'http.https://github.com/kaitranntt/ccs.extraheader',
|
||||
GIT_CONFIG_VALUE_0: header,
|
||||
GIT_CONFIG_KEY_1: 'http.https://github.com/kaitranntt/ccs.git.extraheader',
|
||||
GIT_CONFIG_VALUE_1: header,
|
||||
};
|
||||
|
||||
for (const url of ['https://github.com/kaitranntt/ccs', 'https://github.com/kaitranntt/ccs.git']) {
|
||||
const result = spawnSync('git', ['config', '--get-urlmatch', 'http.extraheader', url], {
|
||||
env,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stdout.trim()).toBe(header);
|
||||
}
|
||||
|
||||
for (const file of ['release.yml', 'dev-release.yml', 'sync-dev-after-release.yml']) {
|
||||
const workflow = fs.readFileSync(path.join(workflowsDir(), file), 'utf8');
|
||||
|
||||
expect(workflow).toContain('echo "::add-mask::${auth_header}"');
|
||||
expect(workflow).toContain('http.https://github.com/kaitranntt/ccs.extraheader');
|
||||
expect(workflow).toContain('http.https://github.com/kaitranntt/ccs.git.extraheader');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ function resolvePath(relativePath: string) {
|
||||
}
|
||||
|
||||
describe('sync dev after release workflow', () => {
|
||||
test('uses protected-branch runner and token settings', () => {
|
||||
test('keeps protected-branch sync on self-hosted runners with scoped push credentials', () => {
|
||||
const workflowPath = resolvePath('../../../../.github/workflows/sync-dev-after-release.yml');
|
||||
|
||||
expect(fs.existsSync(workflowPath)).toBe(true);
|
||||
@@ -22,12 +22,18 @@ describe('sync dev after release workflow', () => {
|
||||
expect(workflow).toContain('name: Sync Dev After Main Release');
|
||||
expect(workflow).toContain('runs-on: [self-hosted, linux, x64]');
|
||||
expect(workflow).not.toContain('runs-on: ubuntu-latest');
|
||||
expect(checkoutSection).toContain('token: ${{ secrets.PAT_TOKEN }}');
|
||||
expect(checkoutSection).not.toContain('token: ${{ github.token }}');
|
||||
expect(checkoutSection).toContain('persist-credentials: false');
|
||||
expect(checkoutSection).not.toContain('token: ${{ secrets.PAT_TOKEN }}');
|
||||
expect(syncSection).toContain(
|
||||
'git merge origin/main --no-edit -m "chore(sync): merge main into dev after release"'
|
||||
);
|
||||
expect(syncSection).not.toContain('[skip ci]');
|
||||
expect(syncSection).toContain('git push origin dev');
|
||||
expect(workflow).toContain('git config --local core.hooksPath /dev/null');
|
||||
expect(syncSection).toContain('PAT_TOKEN: ${{ secrets.PAT_TOKEN }}');
|
||||
expect(syncSection).toContain('echo "::add-mask::${auth_header}"');
|
||||
expect(syncSection).toContain('http.https://github.com/kaitranntt/ccs.extraheader');
|
||||
expect(syncSection).toContain('http.https://github.com/kaitranntt/ccs.git.extraheader');
|
||||
expect(syncSection).toContain('git -c core.hooksPath=/dev/null');
|
||||
expect(syncSection).toContain('push origin dev');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user