Skip to content

Commit aa473cd

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

File tree

5 files changed

+538
-0
lines changed

5 files changed

+538
-0
lines changed

.github/actions/action.js

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
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+
await copyPhpFiles(tempDir, phpFilesDir);
87+
88+
core.info('Removing submodule...');
89+
await safeExec(`git submodule deinit -f -- ${SUBMODULE_PATH}`);
90+
await safeExec(`git rm -f ${SUBMODULE_PATH}`);
91+
await safeExec(`rm -rf .git/modules/${SUBMODULE_PATH}`);
92+
93+
core.info('Restoring filtered PHP files...');
94+
await fs.promises.mkdir(`${SUBMODULE_PATH}`, { recursive: true });
95+
await safeExec(`cp -r ${phpFilesDir}/* ${SUBMODULE_PATH}`);
96+
}
97+
98+
async function copyPhpFiles(sourceDir, destinationDir) {
99+
const phpFiles = [];
100+
const allFiles = await readDirRecursively(sourceDir);
101+
102+
await Promise.all(
103+
allFiles.map(async (filePath) => {
104+
if (filePath.endsWith('.php')) {
105+
const fileName = path.basename(filePath);
106+
const destPath = path.join(destinationDir, fileName);
107+
phpFiles.push(filePath);
108+
await fs.promises.copyFile(filePath, destPath);
109+
}
110+
})
111+
);
112+
113+
return phpFiles;
114+
}
115+
116+
async function createTemporaryBranch() {
117+
const tempBranch = `release-${Date.now()}`;
118+
core.info(`Creating temporary branch ${tempBranch}...`);
119+
await safeExec(`git checkout -b ${tempBranch}`);
120+
}
121+
122+
async function commitAndPushChanges(tagName) {
123+
core.info('Committing changes...');
124+
await safeExec('git add -f ' + SUBMODULE_PATH);
125+
await safeExec('git commit -m "Convert submodule to regular files for release"');
126+
127+
core.info('Updating and pushing tag...');
128+
await safeExec(`git tag -f ${tagName}`);
129+
await safeExec('git push origin --force --tags');
130+
}
131+
132+
async function getTagName(ref) {
133+
if (!ref.startsWith('refs/tags/')) {
134+
core.error(`Invalid ref: ${ref}. This action should be triggered by a tag push.`);
135+
throw new Error('This action expects a tag push event.');
136+
}
137+
const tagName = ref.replace('refs/tags/', '');
138+
core.info(`Tag identified: ${tagName}`);
139+
return tagName;
140+
}
141+
142+
async function createGithubRelease(octokit, tagName, releaseName, context) {
143+
core.info(`Creating release ${releaseName} from tag ${tagName}...`);
144+
const release = await octokit.rest.repos.createRelease({
145+
...context.repo,
146+
tag_name: tagName,
147+
name: releaseName,
148+
body: 'Automated release including submodule files',
149+
draft: false,
150+
prerelease: false
151+
});
152+
153+
core.info('Release created successfully!');
154+
core.setOutput('release-url', release.data.html_url);
155+
}
156+
157+
async function cleanupDirs(directories) {
158+
try {
159+
await Promise.all(
160+
directories.map(async (directory) => {
161+
await fs.promises.rm(directory, { recursive: true, force: true });
162+
core.info(`Successfully cleaned: ${directory}`);
163+
})
164+
);
165+
} catch (error) {
166+
core.warning(`Error during cleanup: ${error.message}`);
167+
}
168+
}
169+
170+
function validateInputs() {
171+
const token = core.getInput('github-token', { required: true });
172+
const gitUserName = core.getInput('git-user-name') || 'GitHub Action';
173+
const gitUserEmail = core.getInput('git-user-email') || '[email protected]';
174+
175+
if (!token) {
176+
throw new Error('A valid GitHub Token is required to authenticate.');
177+
}
178+
179+
return { token, gitUserName, gitUserEmail };
180+
}
181+
182+
async function createDir(directory) {
183+
try {
184+
await fs.promises.mkdir(directory, { recursive: true });
185+
core.info(`Directory created: ${directory}`);
186+
} catch (error) {
187+
core.error(`Failed to create directory: ${directory}`);
188+
throw error;
189+
}
190+
}
191+
192+
async function safeExec(command) {
193+
try {
194+
const { stdout, stderr } = await execAsync(command);
195+
if (stderr) {
196+
core.warning(`Command warning: ${stderr}`);
197+
}
198+
return stdout.trim();
199+
} catch (error) {
200+
core.error(`Command failed: ${command}`);
201+
core.error(`Error: ${error.message}`);
202+
throw error;
203+
}
204+
}

.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)