- Created `qa-test-output-after-fix.txt` and `qa-test-output.txt` to log results of certificate page authentication tests. - Added `build.sh` for deterministic backend builds in CI, utilizing `go list` for efficiency. - Introduced `codeql_scan.sh` for CodeQL database creation and analysis for Go and JavaScript/TypeScript. - Implemented `dockerfile_check.sh` to validate Dockerfiles for base image and package manager mismatches. - Added `sourcery_precommit_wrapper.sh` to facilitate Sourcery CLI usage in pre-commit hooks.
162 lines
7.3 KiB
YAML
162 lines
7.3 KiB
YAML
name: Propagate Changes Between Branches
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
- development
|
|
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
issues: write
|
|
|
|
jobs:
|
|
propagate:
|
|
name: Create PR to synchronize branches
|
|
runs-on: ubuntu-latest
|
|
if: github.actor != 'github-actions[bot]' && github.event.pusher != null
|
|
steps:
|
|
- name: Set up Node (for github-script)
|
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
|
with:
|
|
node-version: '24.11.1'
|
|
|
|
- name: Propagate Changes
|
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
with:
|
|
script: |
|
|
const currentBranch = context.ref.replace('refs/heads/', '');
|
|
|
|
async function createPR(src, base) {
|
|
if (src === base) return;
|
|
|
|
core.info(`Checking propagation from ${src} to ${base}...`);
|
|
|
|
// Check for existing open PRs
|
|
const { data: pulls } = await github.rest.pulls.list({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
state: 'open',
|
|
head: `${context.repo.owner}:${src}`,
|
|
base: base,
|
|
});
|
|
|
|
if (pulls.length > 0) {
|
|
core.info(`Existing PR found for ${src} -> ${base}. Skipping.`);
|
|
return;
|
|
}
|
|
|
|
// Compare commits to see if src is ahead of base
|
|
try {
|
|
const compare = await github.rest.repos.compareCommits({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
base: base,
|
|
head: src,
|
|
});
|
|
|
|
// If src is not ahead, nothing to merge
|
|
if (compare.data.ahead_by === 0) {
|
|
core.info(`${src} is not ahead of ${base}. No propagation needed.`);
|
|
return;
|
|
}
|
|
|
|
// If files changed include history-rewrite or other sensitive scripts,
|
|
// avoid automatic propagation. This prevents bypassing checklist validation
|
|
// and manual review for potentially destructive changes.
|
|
let files = (compare.data.files || []).map(f => (f.filename || '').toLowerCase());
|
|
|
|
// Fallback: if compare.files is empty/truncated, aggregate files from the commit list
|
|
if (files.length === 0 && Array.isArray(compare.data.commits) && compare.data.commits.length > 0) {
|
|
for (const commit of compare.data.commits) {
|
|
const commitData = await github.rest.repos.getCommit({ owner: context.repo.owner, repo: context.repo.repo, ref: commit.sha });
|
|
for (const f of (commitData.data.files || [])) {
|
|
files.push((f.filename || '').toLowerCase());
|
|
}
|
|
}
|
|
files = Array.from(new Set(files));
|
|
}
|
|
|
|
// Load propagation config (list of sensitive paths) from .github/propagate-config.yml when available
|
|
let configPaths = ['scripts/history-rewrite/', 'data/backups', 'docs/plans/history_rewrite.md', '.github/workflows/'];
|
|
try {
|
|
const configResp = await github.rest.repos.getContent({ owner: context.repo.owner, repo: context.repo.repo, path: '.github/propagate-config.yml', ref: src });
|
|
const contentStr = Buffer.from(configResp.data.content, 'base64').toString('utf8');
|
|
const lines = contentStr.split(/\r?\n/);
|
|
let inSensitive = false;
|
|
const parsedPaths = [];
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!inSensitive && trimmed.startsWith('sensitive_paths:')) { inSensitive = true; continue; }
|
|
if (inSensitive) {
|
|
if (trimmed.startsWith('-')) parsedPaths.push(trimmed.substring(1).trim());
|
|
else if (trimmed.length === 0) continue; else break;
|
|
}
|
|
}
|
|
if (parsedPaths.length > 0) configPaths = parsedPaths.map(p => p.toLowerCase());
|
|
} catch (err) { core.info('No .github/propagate-config.yml or parse failure; using defaults.'); }
|
|
|
|
const sensitive = files.some(fn => configPaths.some(sp => fn.startsWith(sp) || fn.includes(sp)));
|
|
if (sensitive) {
|
|
core.info(`${src} -> ${base} contains sensitive changes (${files.join(', ')}). Skipping automatic propagation.`);
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
// If base branch doesn't exist, etc.
|
|
core.warning(`Error comparing ${src} to ${base}: ${error.message}`);
|
|
return;
|
|
}
|
|
|
|
// Create PR
|
|
try {
|
|
const pr = await github.rest.pulls.create({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
title: `Propagate changes from ${src} into ${base}`,
|
|
head: src,
|
|
base: base,
|
|
body: `Automated PR to propagate changes from ${src} into ${base}.\n\nTriggered by push to ${currentBranch}.`,
|
|
draft: true,
|
|
});
|
|
core.info(`Created PR #${pr.data.number} to merge ${src} into ${base}`);
|
|
// Add an 'auto-propagate' label to the created PR and create the label if missing
|
|
try {
|
|
try {
|
|
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: 'auto-propagate' });
|
|
} catch (e) {
|
|
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: 'auto-propagate', color: '7dd3fc', description: 'Automatically created propagate PRs' });
|
|
}
|
|
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.data.number, labels: ['auto-propagate'] });
|
|
} catch (labelErr) {
|
|
core.warning('Failed to ensure or add auto-propagate label: ' + labelErr.message);
|
|
}
|
|
} catch (error) {
|
|
core.warning(`Failed to create PR from ${src} to ${base}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
if (currentBranch === 'main') {
|
|
// Main -> Development
|
|
await createPR('main', 'development');
|
|
} else if (currentBranch === 'development') {
|
|
// Development -> Feature branches
|
|
const branches = await github.paginate(github.rest.repos.listBranches, {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
});
|
|
|
|
const featureBranches = branches
|
|
.map(b => b.name)
|
|
.filter(name => name.startsWith('feature/'));
|
|
|
|
core.info(`Found ${featureBranches.length} feature branches: ${featureBranches.join(', ')}`);
|
|
|
|
for (const featureBranch of featureBranches) {
|
|
await createPR('development', featureBranch);
|
|
}
|
|
}
|
|
env:
|
|
CHARON_TOKEN: ${{ secrets.CHARON_TOKEN }}
|
|
CPMP_TOKEN: ${{ secrets.CPMP_TOKEN }}
|