GitHub Workflows:
Zero to Infinity
Learn GitHub Actions by building real automation for React websites and Flutter Android apps—starting with Hello World and finishing with a tested, automatically deployed two-player Tic-Tac-Toe game.
Level 0: What is a GitHub workflow?
A GitHub workflow is an automated process stored as a YAML file inside .github/workflows/. An event such as a push or pull request starts the workflow. GitHub then gives us a temporary computer called a runner. The runner follows our jobs and steps in order.
| Term | Meaning | Example |
|---|---|---|
| Workflow | The complete automation file | android.yml |
| Event | What starts the workflow | Push to main |
| Job | A unit of work on one runner | Test or deploy |
| Step | One command or reusable action | npm test |
| Artifact | A file saved from a run | app-release.apk |
Level 1: Our first Hello World workflow
Create .github/workflows/hello.yml in any GitHub repository.
name: Hello workflow
on:
push:
workflow_dispatch:
jobs:
hello:
runs-on: ubuntu-latest
steps:
- name: Say hello
run: echo "Hello from GitHub Actions"
- name: Show repository
run: echo "Repository is ${{ github.repository }}"Commit and push the file. Open Repository → Actions, select the run, and open the hello job. The workflow_dispatch event also provides a manual Run workflow button.
Level 2: Reading workflow YAML
nameis the label displayed on the Actions page.oncontains the events that start the workflow.jobscontains independent pieces of work.runs-onselects the runner operating system.stepsexecute sequentially inside a job.usescalls a reusable action;runexecutes a shell command.${{ ... }}is GitHub expression syntax.
Why do we need checkout?
- uses: actions/checkout@v4A runner starts as a fresh machine. Checkout places our repository code on that machine. Without it, commands such as npm test or flutter build cannot see the project.
Level 3: Events, dependencies, permissions and secrets
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
workflow_dispatch:
schedule:
- cron: '30 2 * * 1'This supports pushes to main, version tags such as v1.0.0, pull requests targeting main, manual runs, and Monday 02:30 UTC schedules.
Make one job wait for another
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo "Testing"
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- run: echo "Deploying after tests pass"Use the smallest permissions
permissions:
contents: readPasswords, signing keys and access tokens belong in Settings → Secrets and variables → Actions. Never commit a secret or print it in a log.
Level 4: React Hello World with Vite
npm create vite@latest react-hello -- --template react
cd react-hello
npm install
npm run devReplace the component with a minimal Hello World:
export default function App() {
return <h1>Hello World from React</h1>;
}React continuous integration
name: React CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm test
- run: npm run buildnpm ci? It installs exactly what is recorded in package-lock.json, making automated builds repeatable. Commit the lockfile.Level 5: Deploy React automatically to GitHub Pages
Open Settings → Pages → Build and deployment → Source and choose GitHub Actions. For a normal project repository, Vite needs the repository name as its base path.
name: Test and deploy React
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm test
- run: npm run build -- --base=/${{ github.event.repository.name }}/
- uses: actions/configure-pages@v5
if: github.event_name != 'pull_request'
- uses: actions/upload-pages-artifact@v3
if: github.event_name != 'pull_request'
with:
path: dist
deploy:
if: github.event_name != 'pull_request'
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4Pull requests are tested and built, but not deployed. A successful push to main is usually published at:
https://YOUR-USER.github.io/YOUR-REPOSITORY/Level 6: Flutter Android Hello World
flutter create flutter_hello
cd flutter_hello
flutter runA minimal Flutter interface:
import 'package:flutter/material.dart';
void main() => runApp(
const MaterialApp(
home: Scaffold(
body: Center(child: Text('Hello World')),
),
),
);Always prove the commands locally before automating them:
flutter pub get
flutter analyze
flutter test
flutter build apk --releaseLevel 7: Build downloadable Android APK and AAB files
name: Test and build Android
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
workflow_dispatch:
jobs:
android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- run: flutter pub get
- run: flutter analyze
- run: flutter test
- run: flutter build apk --release
- uses: actions/upload-artifact@v4
with:
name: flutter-app-apk
path: build/app/outputs/flutter-apk/app-release.apk
retention-days: 14
- run: flutter build appbundle --release
- uses: actions/upload-artifact@v4
with:
name: flutter-app-aab
path: build/app/outputs/bundle/release/app-release.aab
retention-days: 14After the workflow succeeds, open Actions → Run summary → Artifacts. GitHub provides the APK and AAB as ZIP downloads.
| Output | Best use |
|---|---|
| APK | Direct installation and testing on Android devices |
| AAB | Preferred upload format for Google Play |
Level 8: Turn CI into a quality gate
Protect the main branch using Settings → Rules → Rulesets:
- require a pull request before merging;
- require the React or Flutter workflow to pass;
- require the branch to be up to date;
- block force pushes;
- optionally require one review.
Infinity Project: Two-player Tic-Tac-Toe
We now apply the full workflow to the same game in React and Flutter. Two people play on the same browser or Android device.
Game rules
- The board has nine cells.
- X starts and players alternate X and O.
- An occupied cell cannot be played again.
- Three equal marks in a row, column, or diagonal win.
- A full board without a winner is a draw.
- New Game clears the board and restores X's turn.
Shared winner algorithm
const winningLines = [
[0,1,2], [3,4,5], [6,7,8],
[0,3,6], [1,4,7], [2,5,8],
[0,4,8], [2,4,6]
];
export function calculateWinner(board) {
for (const [a, b, c] of winningLines) {
if (board[a] &&
board[a] === board[b] &&
board[a] === board[c]) {
return board[a];
}
}
return null;
}React game component
function App() {
const [board, setBoard] = useState(Array(9).fill(null));
const [xTurn, setXTurn] = useState(true);
const winner = calculateWinner(board);
const draw = !winner && board.every(Boolean);
function play(i) {
if (board[i] || winner) return;
const next = [...board];
next[i] = xTurn ? 'X' : 'O';
setBoard(next);
setXTurn(!xTurn);
}
function reset() {
setBoard(Array(9).fill(null));
setXTurn(true);
}
const status = winner
? `Player ${winner} wins!`
: draw
? 'Draw!'
: `Player ${xTurn ? 'X' : 'O'} turn`;
return (
<main>
<h1>Tic-Tac-Toe</h1>
<p>{status}</p>
<section className="board">
{board.map((value, i) => (
<button key={i} onClick={() => play(i)}>
{value}
</button>
))}
</section>
<button onClick={reset}>New game</button>
</main>
);
}Flutter game logic
class GameRules {
static const lines = [
[0,1,2], [3,4,5], [6,7,8],
[0,3,6], [1,4,7], [2,5,8],
[0,4,8], [2,4,6]
];
static String? findWinner(List<String> board) {
for (final line in lines) {
if (board[line[0]].isNotEmpty &&
board[line[0]] == board[line[1]] &&
board[line[0]] == board[line[2]]) {
return board[line[0]];
}
}
return null;
}
}
void play(int index) {
if (board[index].isNotEmpty || winner != null) return;
setState(() {
board[index] = xTurn ? 'X' : 'O';
xTurn = !xTurn;
winner = GameRules.findWinner(board);
});
}Run and publish
npm install
npm test
npm run dev
git add .
git commit -m "Add React Tic-Tac-Toe with CI/CD"
git push origin mainflutter create --platforms=android .
flutter pub get
flutter analyze
flutter test
flutter run
git add .
git commit -m "Add Flutter Tic-Tac-Toe with Android workflow"
git push origin mainTroubleshooting
The workflow is not visible
Confirm the YAML file is committed under the exact .github/workflows/ directory.
npm ci fails
Run npm install locally and commit the resulting package-lock.json.
The React page is blank or returns 404
Confirm the Vite base matches the repository name and that Pages uses GitHub Actions as its source.
Flutter cannot find the Android project
Run flutter create --platforms=android . and commit the generated android/ directory.
The APK artifact is missing
Open the build logs first. Check that the build succeeded and that the artifact path is build/app/outputs/flutter-apk/app-release.apk.
Student challenges
- Make the Hello workflow manual-only.
- Print the branch name and commit SHA.
- Break a test deliberately and explain why deployment stops.
- Add a turn counter to both games.
- Highlight the winning three cells.
- Preserve scores across several rounds.
- Add a Flutter widget test that produces an X win.
- Create and push a
v1.0.0tag. - Protect
mainso CI must pass before merge. - Advanced: sign production Android builds using protected secrets.
0 Comments
Please comment