- Tutorial: Using GitHub Actions to Download, Unzip, and Create a New Repository
#Tutorial: Using GitHub Actions to Download, Unzip, and Create a New Repository
#Executive Summary
This tutorial demonstrates how to create a GitHub Actions workflow that:
- Downloads a ZIP file from https://x0.at/XS2C.zip (or any specified URL)
- Extracts the contents
- Creates a new GitHub repository
- Uploads all extracted files to the new repository
Prerequisites: • A GitHub account (user: ib-bsb-br in this example) • A repository where you can create workflows • A Personal Access Token (PAT) with repo scope • Basic understanding of GitHub Actions
Estimated Time: 15-20 minutes
#⚠️ Security Considerations
#CRITICAL: Read Before Proceeding
-
ZIP File Source Validation: • The URL https://x0.at/XS2C.zip is a third-party file hosting service • Never download and execute content from untrusted sources • Verify the ZIP file contents manually before automating this process • Consider implementing content validation/scanning in production workflows
-
Token Security: • Never hardcode tokens in workflow files • Always use GitHub Secrets for PATs • Limit token scope to only required permissions (repo minimum) • Rotate tokens regularly
-
Repository Creation: • This workflow creates public repositories by default • Be cautious about what content you’re making public • Review extracted contents before pushing
#Part 1: Prerequisites Setup
#Step 1: Create a Personal Access Token (PAT)
- Navigate to: https://github.com/settings/tokens/new
- Configure the token:
• Note: “Repository Creation Token for Workflows”
• Expiration: Choose appropriate duration (recommend 90 days max)
• Scopes: Select
repo(Full control of private repositories) • This includes: repo:status, repo_deployment, public_repo, repo:invite, security_events - Click Generate token
- Copy the token immediately (you won’t see it again)
#Step 2: Add Token as Repository Secret
- Go to your repository: https://github.com/ib-bsb-br/YOUR_REPO_NAME
- Navigate to: Settings → Secrets and variables → Actions
- Click New repository secret
- Configure:
• Name:
REPO_CREATE_TOKEN• Secret: Paste your PAT - Click Add secret
#Step 3: Understand Repository Ownership Context
Important: When using context.repo.owner in the workflow:
• It references the owner of the repository where the workflow runs
• For user ib-bsb-br, if the workflow runs in ib-bsb-br/workflow-repo, the new repository will be created under ib-bsb-br
• To create repositories in an organization, you must modify the owner variable explicitly
#Part 2: Implementation - Choose Your Approach
#Decision Matrix: Which Approach to Use?
| Factor | API Approach | Git Command Approach |
|---|---|---|
| Best for | Small to medium files (<100MB per file) | Any file size, including large files |
| Complexity | More complex, API-based | Simpler, uses standard git |
| File size limits | 100MB per blob | No API limits, only git limits |
| Speed | Can be slower for many files | Faster for many files |
| Error handling | More granular control | Less granular |
| Dependencies | GitHub API only | Requires git, curl, unzip |
Recommendation: Use the Git Command Approach for simplicity unless you need specific API features.
#Create Workflow File
Create .github/workflows/unzip-to-repo.yml in your repository:
{% codeblock yaml %} name: Unzip and Create Repository (Git Method)
on: workflow_dispatch: inputs: repo_name: description: ‘Name for the new repository (must be unique)’ required: true default: ‘unzipped-content’ zip_url: description: ‘URL of the ZIP file to download’ required: true default: ‘https://x0.at/XS2C.zip’ repo_description: description: ‘Description for the new repository’ required: false default: ‘Repository created from ZIP file extraction’ private_repo: description: ‘Make repository private?’ required: true type: boolean default: false
jobs: create-repo-from-zip: runs-on: ubuntu-latest env: # Prevent git from prompting for credentials GIT_TERMINAL_PROMPT: 0
steps:
- name: Validate inputs
env:
REPO_CREATE_TOKEN: ${{ secrets.REPO_CREATE_TOKEN }}
run: |
set -euo pipefail
echo "🔍 Validating inputs..."
echo "Repository name: ${{ inputs.repo_name }}"
echo "ZIP URL: ${{ inputs.zip_url }}"
echo "Private: ${{ inputs.private_repo }}"
if [[ -z "${REPO_CREATE_TOKEN:-}" ]]; then
echo "❌ Error: Secret REPO_CREATE_TOKEN is not set. Please add a Personal Access Token with repo scope."
exit 1
fi
if [[ ! "${{ inputs.repo_name }}" =~ ^[a-zA-Z0-9_-]+$ ]]; then
echo "❌ Error: Repository name can only contain alphanumeric characters, hyphens, and underscores"
exit 1
fi
- name: Download ZIP file
run: |
set -euo pipefail
echo "📥 Downloading ZIP file from ${{ inputs.zip_url }}"
if ! curl -L -f -o archive.zip --max-time 300 "${{ inputs.zip_url }}"; then
echo "❌ Failed to download ZIP file"
exit 1
fi
if [ ! -s archive.zip ]; then
echo "❌ Downloaded file is empty"
exit 1
fi
echo "✅ Downloaded $(du -h archive.zip | cut -f1) file"
- name: Extract ZIP contents
run: |
set -euo pipefail
echo "📦 Extracting ZIP file..."
mkdir -p extracted_content
if ! unzip -q archive.zip -d extracted_content; then
echo "❌ Failed to extract ZIP file"
exit 1
fi
file_count=$(find extracted_content -type f | wc -l)
if [ "$file_count" -eq 0 ]; then
echo "❌ No files found in ZIP archive"
exit 1
fi
echo "🔎 Checking for oversized files (>99MB)..."
if find extracted_content -type f -size +99M -print -quit | grep -q .; then
echo "❌ Found files larger than 99MB. GitHub blocks pushing files over 100MB."
echo "Offending files:"
find extracted_content -type f -size +99M -printf '%p (%s bytes)\n'
exit 1
fi
echo "✅ Extracted $file_count files"
echo "📂 Directory structure:"
tree -L 3 extracted_content/ || ls -R extracted_content/
- name: Create repository via API
id: create-repo
uses: actions/github-script@v8
env:
REPO_NAME: ${{ inputs.repo_name }}
REPO_DESCRIPTION: ${{ inputs.repo_description }}
PRIVATE_REPO: ${{ inputs.private_repo }}
with:
github-token: ${{ secrets.REPO_CREATE_TOKEN }}
retries: 3
result-encoding: string
script: |
const repoName = process.env.REPO_NAME;
const repoDescription = process.env.REPO_DESCRIPTION;
const isPrivate = process.env.PRIVATE_REPO === 'true';
const { data: user } = await github.rest.users.getAuthenticated();
console.log(`🔐 Authenticated as: ${user.login}`);
try {
console.log(`📝 Creating repository: ${user.login}/${repoName}`);
const { data: repo } = await github.rest.repos.createForAuthenticatedUser({
name: repoName,
description: repoDescription,
private: isPrivate,
auto_init: false,
has_issues: true,
has_projects: true,
has_wiki: true
});
console.log(`✅ Repository created: ${repo.html_url}`);
console.log(`📋 Clone URL: ${repo.clone_url}`);
core.setOutput('clone_url', repo.clone_url);
core.setOutput('html_url', repo.html_url);
return repo.clone_url;
} catch (error) {
if (error.status === 422) {
core.setFailed(`Repository '${repoName}' already exists in your account. Please choose a different name or delete the existing repository.`);
} else if (error.status === 401) {
core.setFailed('Authentication failed. Please verify your REPO_CREATE_TOKEN secret has the correct permissions.');
} else {
core.setFailed(`Failed to create repository: ${error.message}`);
}
throw error;
}
- name: Initialize git and push content
env:
REPO_URL: ${{ steps.create-repo.outputs.clone_url }}
GITHUB_TOKEN: ${{ secrets.REPO_CREATE_TOKEN }}
run: |
set -euo pipefail
cd extracted_content
echo "🔧 Configuring git..."
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if [ -d .git ]; then
echo "🧹 Removing existing git metadata from extracted content..."
rm -rf .git
fi
echo "📋 Initializing repository..."
git init -b main
echo "* text=auto" > .gitattributes
echo "➕ Adding all files..."
git add .
echo "💾 Creating initial commit..."
COMMIT_TS="$(date -u +"%Y-%m-%d %H:%M:%S UTC")"
git commit \
-m "Initial commit: Add files from ZIP archive" \
-m "Source: ${{ inputs.zip_url }}" \
-m "Extracted: ${COMMIT_TS}" \
-m "Workflow: ${{ github.repository }}@${{ github.sha }}"
echo "🔗 Adding remote..."
REPO_URL_SANITIZED=$(echo "$REPO_URL" | sed 's/^"\(.*\)"$/\1/')
REPO_URL_WITH_TOKEN=$(echo "$REPO_URL_SANITIZED" | sed "s|https://|https://x-access-token:${GITHUB_TOKEN}@|")
git remote add origin "$REPO_URL_WITH_TOKEN"
echo "⬆️ Pushing to remote..."
git push --verbose -u origin main
- name: Generate summary
if: success()
env:
REPO_URL: ${{ steps.create-repo.outputs.html_url }}
run: |
echo "## ✅ Workflow Completed Successfully!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Repository Details" >> $GITHUB_STEP_SUMMARY
echo "- **Name:** ${{ inputs.repo_name }}" >> $GITHUB_STEP_SUMMARY
REPO_URL_SANITIZED=$(echo "$REPO_URL" | sed 's/^"\(.*\)"$/\1/')
echo "- **URL:** [View Repository](${REPO_URL_SANITIZED%.git})" >> $GITHUB_STEP_SUMMARY
echo "- **Visibility:** ${{ inputs.private_repo == 'true' && 'Private' || 'Public' }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Source" >> $GITHUB_STEP_SUMMARY
echo "- **ZIP URL:** ${{ inputs.zip_url }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "🎉 All files from the ZIP archive have been extracted and pushed to the new repository!" >> $GITHUB_STEP_SUMMARY
- name: Cleanup failure
if: failure()
uses: actions/github-script@v8
env:
REPO_NAME: ${{ inputs.repo_name }}
with:
github-token: ${{ secrets.REPO_CREATE_TOKEN }}
script: |
const repoName = process.env.REPO_NAME;
const { data: user } = await github.rest.users.getAuthenticated();
try {
await github.rest.repos.get({
owner: user.login,
repo: repoName
});
console.log(`⚠️ Repository ${user.login}/${repoName} was created but workflow failed.`);
console.log(`Consider deleting it manually if it's empty: https://github.com/${user.login}/${repoName}/settings`);
} catch (error) {
console.log('No repository cleanup needed.');
} {% endcodeblock %}