Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DEVX-X] Migrate merge-gatekeeper to native action #1

Merged
merged 1 commit into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
29 changes: 29 additions & 0 deletions .github/actions/github-actions/release-action/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!-- action-docs-header source="action.yml" -->

<!-- action-docs-header source="action.yml" -->

<!-- action-docs-description source="action.yml" -->
## Description

Manages GitHub Action releases by validating changes and creating release PRs
<!-- action-docs-description source="action.yml" -->

<!-- action-docs-runs source="action.yml" -->
## Runs

This action is a `composite` action.
<!-- action-docs-runs source="action.yml" -->

<!-- action-docs-inputs source="action.yml" -->
## Inputs

| name | description | required | default |
| --- | --- | --- | --- |
| `mode` | <p>Operation mode: check (for PR validation) or release (for creating releases)</p> | `true` | `""` |
| `base_branch` | <p>Base branch to create a version branch from when the version branch does not exist</p> | `false` | `main` |
| `main_branch` | <p>Main default branch of the repository</p> | `false` | `main` |
<!-- action-docs-inputs source="action.yml" -->

<!-- action-docs-outputs source="action.yml" -->

<!-- action-docs-outputs source="action.yml" -->
189 changes: 189 additions & 0 deletions .github/actions/github-actions/release-action/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
name: 'Action Release Manager'
description: 'Manages GitHub Action releases by validating changes and creating release PRs'

inputs:
mode:
description: 'Operation mode: check (for PR validation) or release (for creating releases)'
required: true
base_branch:
description: 'Base branch to create a version branch from when the version branch does not exist'
required: false
default: 'main'
main_branch:
description: 'Main default branch of the repository'
required: false
default: 'main'

runs:
using: "composite"
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v45

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install Semver
shell: bash
run: |
mkdir -p /tmp/action_node_modules
npm install semver --prefix /tmp/action_node_modules
echo "NODE_PATH=/tmp/action_node_modules/node_modules" >> $GITHUB_ENV
- name: Find and validate action directory
id: find-action
uses: ./.github/actions/github-actions/validate-return-modified-action
with:
changed_files: ${{ steps.changed-files.outputs.all_modified_files }}

- name: Handle action directory validation failure
if: failure()
uses: actions/github-script@v7
with:
script: |
const errorMsg = '${{ steps.find-action.outputs.error }}' || 'Action validation failed';
if ('${{ inputs.mode }}' === 'check') {
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
body: errorMsg,
event: 'REQUEST_CHANGES'
});
}
core.setFailed(errorMsg);
process.exit();
- name: Dismiss PR change requests if all OK
if: inputs.mode == 'check' && steps.find-action.outputs.error == ''
uses: actions/github-script@v7
with:
script: |
const reviews = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
const reviewsToDismiss = reviews.data.filter(r => r.user.login === 'github-actions[bot]' && r.state === 'CHANGES_REQUESTED');
for (const review of reviewsToDismiss) {
await github.rest.pulls.dismissReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
review_id: review.id,
message: 'Dismissing review as it is resolved by a subsequent commit'
});
}
- name: Determine release info
if: inputs.mode == 'release' && steps.find-action.outputs.modified_action != ''
id: release-info
uses: actions/github-script@v7
with:
script: |
const { execSync } = require('child_process');
let modifiedActions;
try {
modifiedActions = JSON.parse('${{ steps.find-action.outputs.modified_action }}');
} catch (e) {
console.log('No actions to release');
process.exit();
}
if (modifiedActions.length === 0) {
console.log('No actions to release');
process.exit();
}
const releaseInfo = modifiedActions.map(action => {
let releaseType;
try {
execSync(`git show "origin/${action.version}:${action.action}"`, { stdio: 'ignore' });
releaseType = "update";
} catch {
releaseType = "release";
}
return {
action_path: action.action,
version: action.version,
release_type: releaseType,
branch_name: `${action.version}-${releaseType}-${action.action.replace('/', '-').replace('\\', '-')}`
};
});
core.setOutput('release_info', JSON.stringify(releaseInfo));
- name: Create version and release branches
if: inputs.mode == 'release' && steps.find-action.outputs.modified_action != ''
shell: bash
run: |
release_info=$(echo '${{ steps.release-info.outputs.release_info }}' | jq -c '.[]')
for info in $release_info; do
version=$(echo $info | jq -r '.version')
branch_name=$(echo $info | jq -r '.branch_name')
action_path=$(echo $info | jq -r '.action_path')
release_type=$(echo $info | jq -r '.release_type')
if ! git show-ref --quiet refs/heads/$version; then
if ! git show-ref --quiet origin/$version; then
git checkout -b ${{ inputs.base_branch }} origin/${{ inputs.base_branch }}
git pull
git checkout -b $version origin/$version
git push origin $version
else
echo "Version branch $version exists remotely, checking out"
git checkout -b $version origin/$version
git pull
fi
else
echo "Version branch $version already exists locally, skipping creation."
git checkout $version
git pull
fi
git config user.name 'Action Release Bot'
git config user.email '[email protected]'
git branch -a
git fetch origin ${{ inputs.main_branch }}
git checkout -b $branch_name
git checkout origin/${{ inputs.main_branch }} -- $action_path
git add .
git commit -m "[$release_type] $version for $action_path"
git push origin $branch_name
done
- name: Create pull requests
if: inputs.mode == 'release' && steps.find-action.outputs.modified_action != ''
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
release_info=$(echo '${{ steps.release-info.outputs.release_info }}' | jq -c '.[]')
for info in $release_info; do
version=$(echo $info | jq -r '.version')
branch_name=$(echo $info | jq -r '.branch_name')
action_path=$(echo $info | jq -r '.action_path')
release_type=$(echo $info | jq -r '.release_type')
gh pr create \
--base "$version" \
--title "[$release_type] $version $release_type for $action_path" \
--head "$branch_name" \
--body "## What has been done:
- $version $release_type for $action_path"
done
git checkout ${{ inputs.main_branch }}
5 changes: 5 additions & 0 deletions .github/actions/github-actions/release-action/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "release-action",
"version": "0.0.2-beta",
"description": "Manages GitHub Action releases by validating changes and creating release PRs"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!-- action-docs-header source="action.yml" -->

<!-- action-docs-header source="action.yml" -->

<!-- action-docs-description source="action.yml" -->
## Description

Finds the GitHub Action directory that was modified in the provided list of changed files
<!-- action-docs-description source="action.yml" -->

<!-- action-docs-runs source="action.yml" -->
## Runs

This action is a `composite` action.
<!-- action-docs-runs source="action.yml" -->

<!-- action-docs-inputs source="action.yml" -->
## Inputs

| name | description | required | default |
| --- | --- | --- | --- |
| `changed_files` | <p>List of changed files</p> | `true` | `""` |
<!-- action-docs-inputs source="action.yml" -->

<!-- action-docs-outputs source="action.yml" -->
## Outputs

| name | description |
| --- | --- |
| `modified_action` | <p>Path to the modified action directory</p> |
| `error` | <p>Error message if the action directory validation failed</p> |
<!-- action-docs-outputs source="action.yml" -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: 'Find Modified Action'
description: 'Finds the GitHub Action directory that was modified in the provided list of changed files'

inputs:
changed_files:
description: 'List of changed files'
required: true

outputs:
modified_action:
description: 'Path to the modified action directory'
value: ${{ steps.find-action.outputs.modified_action }}

error:
description: 'Error message if the action directory validation failed'
value: ${{ steps.find-action.outputs.error }}

runs:
using: "composite"
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install Semver
shell: bash
run: |
# Install semver in a separate directory to avoid polluting the git branch
mkdir -p /tmp/action_node_modules
npm install semver --prefix /tmp/action_node_modules
echo "NODE_PATH=/tmp/action_node_modules/node_modules" >> $GITHUB_ENV
- name: Find and validate action directory
id: find-action
uses: actions/github-script@v7
env:
NODE_PATH: /tmp/action_node_modules/node_modules
with:
script: |
const fs = require('fs');
const path = require('path');
const semver = require('semver');
function findModifiedActionDir(changedFiles) {
/* For all changed files, find if they are part of an action directory
and return the set of all unique action directories */
const modifiedActionDirs = new Set();
changedFiles.forEach(file => {
let currentDir = path.dirname(file);
// Walk up directory tree until we find action.yml/yaml or reach root
while (currentDir !== '.' && currentDir !== '/') {
if (
fs.existsSync(path.join(currentDir, 'action.yml')) ||
fs.existsSync(path.join(currentDir, 'action.yaml'))
) {
modifiedActionDirs.add(currentDir);
break;
}
currentDir = path.dirname(currentDir);
}
});
return modifiedActionDirs;
}
function validateActionDir(actionDir) {
/* Validate action directory's package.json */
// Validate package.json exists
if (!fs.existsSync(path.join(actionDir, 'package.json'))) {
throw new Error('The action directory does not contain package.json file');
}
// Validate package.json content
try {
const packageJson = JSON.parse(fs.readFileSync(path.join(actionDir, 'package.json'), 'utf8'));
if (!packageJson.version) {
throw new Error('The package.json file does not contain a version field');
}
if (!semver.valid(packageJson.version)) {
throw new Error('The package.json file contains an invalid version field');
}
} catch (error) {
throw new Error(`Package.json validation failed: ${error.message}`);
}
}
let changedFiles = `${{ inputs.changed_files }}`.split(' ');
const modifiedActionDirs = findModifiedActionDir(changedFiles);
// Exit if there are no modified action directories
if (modifiedActionDirs.size === 0) {
console.log('No action to release');
return;
}
for (const targetActionDir of modifiedActionDirs) {
try {
validateActionDir(targetActionDir);
} catch (error) {
core.setFailed(error.message);
core.setOutput('error', error.message);
return;
}
}
// Get the action directory and version
const actionDirsOutput = Array.from(modifiedActionDirs).map(
dir => {
const packageJson = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
return {
"action": dir,
"version": semver.prerelease(packageJson.version) ? semver.prerelease(packageJson.version)[0] : "v" + semver.major(packageJson.version)
}
}
);
// Set the output
core.setOutput('modified_action', JSON.stringify(actionDirsOutput));
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "validate-return-modified-action",
"version": "0.0.2-beta",
"description": "Finds the GitHub Action directory that was modified in the provided list of changed files"
}
Loading
Loading