4 files changed
+134
-25
No files matched your search
@@ -0,0 +1,84 @@
|
||||
name: Mirror GitHub release to GitCode
|
||||
|
||||
run-name: >-
|
||||
GitCode mirror ${{ inputs.release_tag || github.event.workflow_run.display_title }}
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Build portable releases
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: Existing GitHub release tag to mirror
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: gitcode-mirror-${{ inputs.release_tag || github.event.workflow_run.id }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
name: Mirror release assets
|
||||
if: >-
|
||||
vars.GITCODE_REPOSITORY != ''
|
||||
&& (
|
||||
github.event_name == 'workflow_dispatch'
|
||||
|| github.event.workflow_run.conclusion == 'success'
|
||||
)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Resolve release tag
|
||||
id: release
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
RUN_TITLE: ${{ github.event.workflow_run.display_title }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
release_tag="$INPUT_RELEASE_TAG"
|
||||
if [ -z "$release_tag" ]; then
|
||||
case "$RUN_TITLE" in
|
||||
"Portable release "*) release_tag="${RUN_TITLE#Portable release }" ;;
|
||||
*) echo "Cannot resolve a release tag from workflow title: $RUN_TITLE" >&2; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
case "$release_tag" in
|
||||
v*) ;;
|
||||
*) release_tag="v$release_tag" ;;
|
||||
esac
|
||||
if ! node scripts/app-version.mjs "${release_tag#v}" >/dev/null 2>&1; then
|
||||
if [ -n "$INPUT_RELEASE_TAG" ]; then
|
||||
echo "Invalid release tag: $release_tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Skipping GitCode mirror because the completed run did not publish a release"
|
||||
echo "should_mirror=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "tag=$release_tag" >> "$GITHUB_OUTPUT"
|
||||
echo "should_mirror=true" >> "$GITHUB_OUTPUT"
|
||||
- name: Download GitHub release assets
|
||||
if: steps.release.outputs.should_mirror == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: gh release download "${{ steps.release.outputs.tag }}" --dir artifacts
|
||||
- name: Publish GitCode mirror
|
||||
if: steps.release.outputs.should_mirror == 'true'
|
||||
env:
|
||||
ARTIFACT_DIRECTORY: artifacts
|
||||
GITCODE_ACCESS_TOKEN: ${{ secrets.GITCODE_ACCESS_TOKEN }}
|
||||
GITCODE_REPOSITORY: ${{ vars.GITCODE_REPOSITORY }}
|
||||
GITCODE_TARGET_COMMITISH: ${{ vars.GITCODE_TARGET_COMMITISH || 'main' }}
|
||||
RELEASE_TAG: ${{ steps.release.outputs.tag }}
|
||||
run: node scripts/publish-gitcode-release.mjs
|
||||
@@ -391,7 +391,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
publish:
|
||||
name: Publish releases
|
||||
name: Publish GitHub release
|
||||
if: >-
|
||||
(github.event_name == 'release' && github.event.release.prerelease == true)
|
||||
|| startsWith(github.ref, 'refs/tags/')
|
||||
@@ -400,7 +400,7 @@ jobs:
|
||||
- version
|
||||
- build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
@@ -461,13 +461,3 @@ jobs:
|
||||
else
|
||||
gh release create "$RELEASE_TAG" "${release_args[@]}" artifacts/*
|
||||
fi
|
||||
- name: Publish GitCode mirror
|
||||
if: ${{ vars.GITCODE_REPOSITORY != '' }}
|
||||
env:
|
||||
ARTIFACT_DIRECTORY: artifacts
|
||||
GITCODE_ACCESS_TOKEN: ${{ secrets.GITCODE_ACCESS_TOKEN }}
|
||||
GITCODE_REPOSITORY: ${{ vars.GITCODE_REPOSITORY }}
|
||||
GITCODE_TARGET_COMMITISH: ${{ vars.GITCODE_TARGET_COMMITISH || 'main' }}
|
||||
PRERELEASE: ${{ github.event_name == 'release' && github.event.release.prerelease || inputs.prerelease }}
|
||||
RELEASE_TAG: ${{ needs.version.outputs.tag }}
|
||||
run: node scripts/publish-gitcode-release.mjs
|
||||
@@ -1,4 +1,5 @@
|
||||
import { readdir, readFile } from 'node:fs/promises';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import { basename, join, resolve } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
@@ -46,6 +47,39 @@ async function apiRequest(path, {
|
||||
return response.status === 204 || response.status === 404 ? null : response.json();
|
||||
}
|
||||
|
||||
export function uploadFileWithCurl({ url, headers, path }) {
|
||||
const args = [
|
||||
'--fail-with-body',
|
||||
'--silent',
|
||||
'--show-error',
|
||||
'--location',
|
||||
'--retry', '3',
|
||||
'--retry-delay', '5',
|
||||
'--retry-all-errors',
|
||||
'--connect-timeout', '30',
|
||||
'--max-time', '1800',
|
||||
'--request', 'PUT',
|
||||
'--upload-file', path,
|
||||
'--output', process.platform === 'win32' ? 'NUL' : '/dev/null',
|
||||
];
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
args.push('--header', `${name}: ${value}`);
|
||||
}
|
||||
args.push(url);
|
||||
|
||||
return new Promise((resolveUpload, rejectUpload) => {
|
||||
const child = spawn('curl', args, { stdio: ['ignore', 'inherit', 'inherit'] });
|
||||
child.once('error', rejectUpload);
|
||||
child.once('exit', (code, signal) => {
|
||||
if (code === 0) {
|
||||
resolveUpload();
|
||||
} else {
|
||||
rejectUpload(new Error(`curl upload failed with ${signal ? `signal ${signal}` : `exit code ${code}`}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function publishGitcodeRelease({
|
||||
directory,
|
||||
repository,
|
||||
@@ -54,6 +88,7 @@ export async function publishGitcodeRelease({
|
||||
targetCommitish = 'main',
|
||||
prerelease = false,
|
||||
fetchImpl = fetch,
|
||||
uploadImpl = uploadFileWithCurl,
|
||||
}) {
|
||||
const [owner, repo] = parseRepository(repository);
|
||||
if (!token) throw new Error('GITCODE_ACCESS_TOKEN is required');
|
||||
@@ -112,15 +147,11 @@ export async function publishGitcodeRelease({
|
||||
const headers = Array.isArray(rawHeaders)
|
||||
? Object.fromEntries(rawHeaders.map(({ key, name, value }) => [key ?? name, value]))
|
||||
: rawHeaders && typeof rawHeaders === 'object' ? rawHeaders : {};
|
||||
const response = await fetchImpl(uploadUrl, {
|
||||
method: 'PUT',
|
||||
await uploadImpl({
|
||||
url: uploadUrl,
|
||||
headers,
|
||||
body: await readFile(join(directory, entry.name)),
|
||||
redirect: 'follow',
|
||||
path: join(directory, entry.name),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Uploading ${entry.name} to GitCode failed: HTTP ${response.status}`);
|
||||
}
|
||||
console.log(`Uploaded GitCode asset: ${entry.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ describe('GitCode release mirror', () => {
|
||||
test('creates a release and uploads compiled artifacts', async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'easycli-gitcode-release-'));
|
||||
const calls: Array<{ url: string; method: string; body: unknown }> = [];
|
||||
const uploads: Array<{ url: string; path: string }> = [];
|
||||
try {
|
||||
await writeFile(join(directory, 'EasyCLIProxyAPI-v1.2.3-Windows-amd64.zip'), 'artifact');
|
||||
const fetchImpl = async (input: URL | RequestInfo, init: RequestInit = {}) => {
|
||||
@@ -26,9 +27,6 @@ describe('GitCode release mirror', () => {
|
||||
headers: { 'x-upload-token': 'signed' },
|
||||
});
|
||||
}
|
||||
if (method === 'PUT' && url === 'https://file-cdn.gitcode.com/presigned-upload') {
|
||||
return new Response('', { status: 200 });
|
||||
}
|
||||
return new Response('', { status: 500 });
|
||||
};
|
||||
|
||||
@@ -38,13 +36,19 @@ describe('GitCode release mirror', () => {
|
||||
token: 'secret-token',
|
||||
tag: 'v1.2.3',
|
||||
fetchImpl,
|
||||
uploadImpl: async ({ url, path }) => {
|
||||
uploads.push({ url, path });
|
||||
},
|
||||
});
|
||||
|
||||
expect(calls.map(({ method }) => method)).toEqual(['GET', 'POST', 'GET', 'PUT']);
|
||||
expect(calls.map(({ method }) => method)).toEqual(['GET', 'POST', 'GET']);
|
||||
expect(calls[0].url).toContain('/repos/mirror-owner/EasyCLIProxyAPI/releases/tags/v1.2.3');
|
||||
expect(calls[0].url).toContain('access_token=secret-token');
|
||||
expect(calls[2].url).toContain('file_name=EasyCLIProxyAPI-v1.2.3-Windows-amd64.zip');
|
||||
expect(calls[3].body).toBeInstanceOf(Uint8Array);
|
||||
expect(uploads).toEqual([{
|
||||
url: 'https://file-cdn.gitcode.com/presigned-upload',
|
||||
path: join(directory, 'EasyCLIProxyAPI-v1.2.3-Windows-amd64.zip'),
|
||||
}]);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user