GitHub Actions to Download, Unzip, and Create a New Repository

Slug: github-actions-unzip-tutorial

11426 characters 1295 words

#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:

  1. Downloads a ZIP file from https://x0.at/XS2C.zip (or any specified URL)
  2. Extracts the contents
  3. Creates a new GitHub repository
  4. 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

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

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

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

  1. Navigate to: https://github.com/settings/tokens/new
  2. 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
  3. Click Generate token
  4. Copy the token immediately (you won’t see it again)

#Step 2: Add Token as Repository Secret

  1. Go to your repository: https://github.com/ib-bsb-br/YOUR_REPO_NAME
  2. Navigate to: Settings → Secrets and variables → Actions
  3. Click New repository secret
  4. Configure: • Name: REPO_CREATE_TOKENSecret: Paste your PAT
  5. 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 %}
URL: https://ib.bsb.br/github-actions-unzip-tutorial