Continuous integration repeats the same checks for each change. This example runs Python tests without external packages and creates a ZIP only after those tests pass. Keep the checked commit and resulting artifact connected so a successful run has a clear meaning.
You should be able to push your code to a GitHub repository. If you only use local Gitea, you can choose the Jenkins route in the next section. I’ve verified local Python testing and file creation, but there’s no record of actually running this workflow from a hosted runner on my GitHub account.
Git · GitOps All Series · 17 / 24
On this page
1. Create a test to determine success and failure
Save test_greeting.py in the same directory as greeting.py in Part 5. In addition to valid input, it also removes whitespace and checks for invalid input. If you have too few tests, even if your CI is green, it doesn’t describe the actual quality.
import unittest
from greeting import greet
class GreetingTest(unittest.TestCase):
def test_greeting(self):
self.assertEqual(greet("Git"), "Hello, Git!")
def test_trims_spaces(self):
self.assertEqual(greet(" Rocky "), "Hello, Rocky!")
def test_rejects_empty_name(self):
with self.assertRaises(ValueError):
greet(" ")
if __name__ == "__main__":
unittest.main()
# Linux or Git Bash with a working python3 command:
python3 -m unittest discover -v
In Windows PowerShell, you can use py -3 -m unittest discover -v. Three tests must pass: app/ in the example bundle contains code and tests. Copy it to the repository root to use.
2. Record the workflow in Git
Save the contents below to .github/workflows/ci.yml in the repository root. Actions reads YAML from this path. If you create another arbitrary parent folder in front, it may not be recognized.
name: python-ci
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Test
run: python -m unittest discover -v
- name: Package
run: |
mkdir -p dist
git archive --format=zip --output=dist/app.zip HEAD
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: app-${{ github.sha }}
path: dist/app.zip
retention-days: 7
Events are a push from main and a PR to main. A job is a set of tasks executed on a single runner, and steps are individual commands within it. A new runner does not automatically have the repository, so a checkout step is required.
Request only the storage read permission required for this example with permissions: contents: read. persist-credentials: false prevents authentication information from being left in Git settings after checkout. The action was anchored to the full commit SHA seen in the official release on October 18, 2026. When updating a version, review the relationship between the release and the SHA.
3. After pushing, check which commit is being executed
git add greeting.py test_greeting.py .github/workflows/ci.yml .gitignore .gitattributes
git diff --staged --check
git commit -m "Add Python CI and package artifact"
git push origin main
If .gitignore and .gitattributes do not exist, save the rules in Part 13 first or exclude them from being added. Check the execution events, commit IDs, and Test logs in the server’s Actions tab. If a branch has protection rules, it will proceed with a PR instead of pushing directly to main.
If the test passes, archive dist/app.zip. git archive is an example of bundling a trace file for that commit, not a command to build a Python distribution package. The artifact retention period is set to 7 days to distinguish it from long-term release repositories.
4. Test changes that fail
Try removing the space stripping of greet on a separate branch, or replace the expected string incorrectly and open the PR. If the test step fails, subsequent packages and uploads should not be executed. Push the code again and see if the new commit’s check succeeds.
For PR checking, I don’t just copy the configuration that checks out the external PR code to pull_request_target and passes the secret value. Separate operational deployment permissions from the path through which untrusted code runs. In your own runner, you need to check the extent to which you can access networks and hosts.
For the basic syntax, refer to GitHub Actions Quick Start, and for permission and action fixation principles, refer to GitHub Actions Security Guide. Only after CI is successful do we move on to the CD phase, where we decide which deliverables to deploy.
Previous · [Git and GitOps 16] Back Up and Restore Gitea Beyond the Repository Files · Next · [Git and GitOps 18] Connect Gitea and Jenkins: Credentials and Jenkinsfile · Series list and example files



