Skip to content

Commit f933dd8

Browse files
committed
Add GitHub Action for automated releases with submodule file inclusion.
1 parent ae750b3 commit f933dd8

File tree

5 files changed

+559
-0
lines changed

5 files changed

+559
-0
lines changed

.github/actions/action.js

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
const core = require('@actions/core');
2+
const github = require('@actions/github');
3+
const {exec} = require('child_process');
4+
const fs = require('fs');
5+
const path = require('path');
6+
const util = require('util');
7+
const execAsync = util.promisify(exec);
8+
const SUBMODULE_PATH = 'meta/attributes/public';
9+
const TEMP_DIR = process.env.TEMP_DIR || 'temp_submodule';
10+
const PHP_FILES_DIR = process.env.PHP_FILES_DIR || 'filtered_submodule';
11+
12+
async function run() {
13+
try {
14+
const { token, gitUserName, gitUserEmail } = validateInputs();
15+
const octokit = github.getOctokit(token);
16+
const context = github.context;
17+
const tagName = await getTagName(context.ref);
18+
const releaseName = `PhpStorm ${tagName.replace('v', '')}`;
19+
20+
await configureGit(gitUserName, gitUserEmail);
21+
22+
try {
23+
await createTemporaryBranch();
24+
await manageSubmoduleFiles(TEMP_DIR, PHP_FILES_DIR);
25+
} finally {
26+
core.info('Cleaning up temporary directories...');
27+
await cleanupDirs([TEMP_DIR, PHP_FILES_DIR]);
28+
}
29+
30+
await commitAndPushChanges(tagName);
31+
32+
await createGithubRelease(octokit, tagName, releaseName, context);
33+
34+
} catch (error) {
35+
core.error(`Run failed: ${error.message}`);
36+
core.setFailed(error.message);
37+
}
38+
}
39+
40+
// Top-level error handling
41+
run().catch(error => {
42+
core.error(`Unhandled error: ${error.message}`);
43+
core.setFailed(error.message);
44+
});
45+
46+
/**
47+
* @param {string} dir - The directory to start reading from.
48+
* @returns {Array<string>} - A flat list of all file paths.
49+
*/
50+
async function readDirRecursively(dir) {
51+
try {
52+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
53+
const files = await Promise.all(entries.map(async (entry) => {
54+
const fullPath = path.join(dir, entry.name);
55+
return entry.isDirectory() ? await readDirRecursively(fullPath) : fullPath;
56+
}));
57+
return files.flat();
58+
} catch (error) {
59+
core.error(`Error reading directory ${dir}: ${error.message}`);
60+
throw error;
61+
}
62+
}
63+
64+
65+
async function configureGit(gitUserName, gitUserEmail) {
66+
core.info('Configuring Git...');
67+
try {
68+
await safeExec(`git config --global user.name "${gitUserName}"`);
69+
await safeExec(`git config --global user.email "${gitUserEmail}"`);
70+
core.info(`Git configured successfully with user: ${gitUserName}, email: ${gitUserEmail}`);
71+
} catch (error) {
72+
core.error('Failed to configure Git.');
73+
core.setFailed(error.message);
74+
throw error;
75+
}
76+
}
77+
78+
async function manageSubmoduleFiles(tempDir, phpFilesDir) {
79+
core.info('Initializing and updating submodule...');
80+
await safeExec('git submodule update --init --recursive');
81+
82+
core.info('Saving submodule files...');
83+
await createDir(tempDir)
84+
await createDir(phpFilesDir);
85+
await safeExec(`cp -r ${SUBMODULE_PATH}/* ${tempDir}`);
86+
87+
await copyPhpFiles(tempDir, phpFilesDir);
88+
89+
/*core.info(`Reading contents of ${tempDir} recursively...`);
90+
const allFiles = await readDirRecursively(tempDir);
91+
core.info(`Files read: ${allFiles.length}`);
92+
93+
core.info('Filtering PHP files...');
94+
allFiles.forEach(filePath => {
95+
core.info(`Processing file: ${filePath}`);
96+
if (filePath.endsWith('.php')) {
97+
const fileName = path.basename(filePath);
98+
const destPath = path.join(phpFilesDir, fileName);
99+
fs.copyFileSync(filePath, destPath);
100+
}
101+
});
102+
103+
if (fs.readdirSync(phpFilesDir).length === 0) {
104+
core.info('No PHP files found during filtering.');
105+
} else {
106+
core.info('PHP files successfully filtered and copied.');
107+
}*/
108+
109+
core.info('Removing submodule...');
110+
await safeExec(`git submodule deinit -f -- ${SUBMODULE_PATH}`);
111+
await safeExec(`git rm -f ${SUBMODULE_PATH}`);
112+
await safeExec(`rm -rf .git/modules/${SUBMODULE_PATH}`);
113+
114+
core.info('Restoring filtered PHP files...');
115+
await fs.promises.mkdir(`${SUBMODULE_PATH}`, { recursive: true });
116+
await safeExec(`cp -r ${phpFilesDir}/* ${SUBMODULE_PATH}`);
117+
}
118+
119+
async function copyPhpFiles(sourceDir, destinationDir) {
120+
const phpFiles = [];
121+
const allFiles = await readDirRecursively(sourceDir);
122+
123+
await Promise.all(
124+
allFiles.map(async (filePath) => {
125+
if (filePath.endsWith('.php')) {
126+
const fileName = path.basename(filePath);
127+
const destPath = path.join(destinationDir, fileName);
128+
phpFiles.push(filePath);
129+
await fs.promises.copyFile(filePath, destPath);
130+
}
131+
})
132+
);
133+
134+
return phpFiles;
135+
}
136+
137+
async function createTemporaryBranch() {
138+
const tempBranch = `release-${Date.now()}`;
139+
core.info(`Creating temporary branch ${tempBranch}...`);
140+
await safeExec(`git checkout -b ${tempBranch}`);
141+
}
142+
143+
async function commitAndPushChanges(tagName) {
144+
core.info('Committing changes...');
145+
await safeExec('git add -f ' + SUBMODULE_PATH);
146+
await safeExec('git commit -m "Convert submodule to regular files for release"');
147+
148+
core.info('Updating and pushing tag...');
149+
await safeExec(`git tag -f ${tagName}`);
150+
await safeExec('git push origin --force --tags');
151+
}
152+
153+
async function getTagName(ref) {
154+
if (!ref.startsWith('refs/tags/')) {
155+
core.error(`Invalid ref: ${ref}. This action should be triggered by a tag push.`);
156+
throw new Error('This action expects a tag push event.');
157+
}
158+
const tagName = ref.replace('refs/tags/', '');
159+
core.info(`Tag identified: ${tagName}`);
160+
return tagName;
161+
}
162+
163+
async function createGithubRelease(octokit, tagName, releaseName, context) {
164+
core.info(`Creating release ${releaseName} from tag ${tagName}...`);
165+
const release = await octokit.rest.repos.createRelease({
166+
...context.repo,
167+
tag_name: tagName,
168+
name: releaseName,
169+
body: 'Automated release including submodule files',
170+
draft: false,
171+
prerelease: false
172+
});
173+
174+
core.info('Release created successfully!');
175+
core.setOutput('release-url', release.data.html_url);
176+
}
177+
178+
async function cleanupDirs(directories) {
179+
try {
180+
await Promise.all(
181+
directories.map(async (directory) => {
182+
await fs.promises.rm(directory, { recursive: true, force: true });
183+
core.info(`Successfully cleaned: ${directory}`);
184+
})
185+
);
186+
} catch (error) {
187+
core.warning(`Error during cleanup: ${error.message}`);
188+
}
189+
}
190+
191+
function validateInputs() {
192+
const token = core.getInput('github-token', { required: true });
193+
const gitUserName = core.getInput('git-user-name') || 'GitHub Action';
194+
const gitUserEmail = core.getInput('git-user-email') || '[email protected]';
195+
196+
if (!token) {
197+
throw new Error('A valid GitHub Token is required to authenticate.');
198+
}
199+
200+
return { token, gitUserName, gitUserEmail };
201+
}
202+
203+
async function createDir(directory) {
204+
try {
205+
await fs.promises.mkdir(directory, { recursive: true });
206+
core.info(`Directory created: ${directory}`);
207+
} catch (error) {
208+
core.error(`Failed to create directory: ${directory}`);
209+
throw error;
210+
}
211+
}
212+
213+
async function safeExec(command) {
214+
try {
215+
const { stdout, stderr } = await execAsync(command);
216+
if (stderr) {
217+
core.warning(`Command warning: ${stderr}`);
218+
}
219+
return stdout.trim();
220+
} catch (error) {
221+
core.error(`Command failed: ${command}`);
222+
core.error(`Error: ${error.message}`);
223+
throw error;
224+
}
225+
}

.github/actions/action.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
name: 'Create Release with Submodule Files'
2+
description: 'Creates a GitHub release including files from a specified submodule'
3+
inputs:
4+
github-token:
5+
description: 'GitHub token for authentication'
6+
required: true
7+
runs:
8+
using: 'node16'
9+
main: 'action.js'

0 commit comments

Comments
 (0)