Automate a Reliable Container Release
Package a FastAPI API with container CI and a versioned GHCR release.
Introduction
30 Second Summary
Software that works on your computer can still break for the next person. A trustworthy release carries proof that the same package passed its tests.
In this project, you will begin Part 2 of App Dev by packaging the Part 1 FastAPI inventory API as a reproducible container image. You will use continuous integration in GitHub Actions to block a regression before publishing version v2.0.0 through GitHub Container Registry.
What You'll Build
Picture another developer seeing {"status":"ok"} from the public v2.0.0 container release you shipped.
By the end of this project, you'll have:
- A portable API release that runs without relying on your Windows virtual environment.
- A pull-request safety check that turns red when the deliberate inventory regression reaches pytest.
- A public versioned release tagged v2.0.0 that another developer can pull to reproduce the same health response.
- Secret Mission: Add an artifact attestation that records where the released container came from.
Are there any prerequisites?
You'll need the completed Part 1 project on Windows with its four core tests passing. The optional fifth test from the Part 1 Secret Mission is also supported.
Before We Start
This opening checkpoint gives you a moment to commit to the release you are about to create. Your goal is to turn the completed Part 1 API into a tested container artifact that blocks broken builds from shipping.
Verify the Windows Delivery Toolchain
The Part 1 API already works in one Windows environment. A reliable release needs a verified delivery toolchain.
This step checks each tool before release work begins. You will finish with a clean pytest baseline from the existing project.
In this step, get ready to:
- Prove that source-control access is ready.
- Confirm that the container engine is running.
- Lock in the inherited test baseline.
Verify Git and GitHub access
Git records the project history that later workflows will inspect. GitHub will host the public repository that owns those workflows.
- In Visual Studio Code, select Terminal from the menu bar.
- Select New Terminal.
- Check whether Git is available by running this command:
git --version
What does this check prove?
This command asks the Git client to identify its installed version. A version response confirms that PowerShell can find Git.
✔️ Git prints a version
Git is available in this terminal. Your source-control client is ready for the repository work later in the project.
ⓧ Command not found
Git for Windows is missing from the command path. The official Windows package installs Git with Git Credential Manager for browser-based GitHub authentication.
- Review any package-source terms that Windows displays.
- Install the current Git for Windows build by running this command:
winget install --id Git.Git -e --source winget
What does this command do?
Windows Package Manager finds the exact Git for Windows package. It installs the current release from the configured source.
- Close the current terminal session.
- Select New Terminal from the Terminal menu.
- Confirm that the new terminal can find Git by running:
git --version
What should you see?
Git prints its installed version details. This confirms that the refreshed terminal received the updated command path.
🙋♀️ Git still unavailable?
- Restart Visual Studio Code so it reloads the Windows command path.
- Run the version check again in a new terminal.
- Share the non-sensitive installation error in the NextWork community if the command still fails.
Ask for help with a Git installation that PowerShell cannot find.
- Open the GitHub website in your browser.
- Sign in to the GitHub account that will own the public repository.
- Confirm that your account page opens without another sign-in request.
Unable to reach your GitHub account?
Complete the account recovery flow in your browser if GitHub rejects your credentials. Use the account you want associated with the public container package later.
Ask for help with restoring GitHub account access safely.
Start Docker Desktop
Docker Desktop supplies the container engine used by local builds. Its WSL 2 backend provides the Linux environment required by the image in the next step.
- Check the current Docker Desktop state by running this command in PowerShell:
docker version
Why are there two Docker sections?
The client section proves that the Docker command is installed. The server section proves that Docker Desktop has started its container engine.
✔️ I see client and server details
Docker Desktop is running. The command can communicate with the container engine.
ⓧ I see client details only
The Docker command is installed. Docker Desktop still needs to start its container engine.
- Press the Windows key to open Windows search.
- Type Docker Desktop and press Enter.
The first start can take a few minutes while Docker Desktop prepares its Linux environment. A quiet startup screen during that time is expected.
- Check the engine again after Docker Desktop reports that it is running:
docker version
What should change?
The output now includes information from both the Docker client and the Docker server. That second section confirms that the engine is accepting commands.
🙋♀️ Docker engine still unavailable?
- Wait for Docker Desktop to finish starting.
- Restart Docker Desktop if its status remains unchanged.
Ask for help with a Docker client that cannot reach its server.
ⓧ Command not found
Docker Desktop needs to be installed before PowerShell can use the Docker command. Qualifying personal use and education use are covered by Docker Desktop's free terms.
- Open the official Docker Desktop installation guide.
- Download Docker Desktop Installer.exe.
- Run the downloaded installer from your Windows downloads folder.
- Keep the recommended per-user installation mode.
- Select the WSL 2 backend on the configuration page.
- Complete the installation.
Windows may request permission while enabling the backend. The first launch may also take a few minutes while Docker Desktop prepares its environment.
- Press the Windows key to open Windows search.
- Type Docker Desktop and press Enter.
- Confirm that the installation is ready by running:
docker version
What should you see?
PowerShell prints details for the Docker client and server. This confirms that the command and container engine are both ready.
🙋♀️ Docker Desktop installation failed?
- Confirm that hardware virtualization is enabled for the Windows computer.
- Confirm that the computer meets Docker Desktop's current Windows requirements.
- Share the non-sensitive installer message in the NextWork community if installation remains blocked.
Ask for help with a blocked Docker Desktop installation.
- Select the Settings gear in Docker Desktop.
- Select General.
- Confirm that Use the WSL 2 based engine is selected when the option is visible.
- Select Apply if Docker Desktop asks you to save a change.
Docker Desktop may hide the backend setting when Windows supports only the default option. In that case, the installer has already selected the supported backend.
Confirm the inherited test baseline
A passing baseline proves that the inherited API remains healthy before container files are added. The existing Visual Studio Code workspace keeps this check tied to the exact Part 1 project.
- Switch back to the open reliable-inventory-sync-api workspace in Visual Studio Code.
- Select Terminal from the menu bar.
- Select New Terminal.
- Activate the same Python 3.14.7 environment used in Part 1 if the terminal does not activate it automatically.
- Confirm that the terminal path ends with reliable-inventory-sync-api.
Before you run the final checks, which result would reveal that the container engine is installed but still offline?
- Verify Git, Docker Desktop, and the inherited tests together by running:
git --version
docker version
pytest
What do these checks prove?
- The Git result proves that Windows can find the source-control client.
- The Docker result proves that the client can communicate with the running server.
- The pytest result proves that the inherited API still passes its established behavior checks.
You should see Git version details and Docker client information. You should also see Docker server information.
Pytest 9.1.1 reports four passing tests. It reports five passing tests if you completed the Part 1 Secret Mission.
Tests no longer pass?
- Confirm that the terminal is using the Part 1 Python environment.
- Confirm that the terminal path ends with reliable-inventory-sync-api.
- Read the first failing test name before changing any project file.
Ask for help with restoring the inherited pytest baseline.
You now have a clean delivery starting line. Git can track the project, Docker can build containers, and the inherited tests protect the API behavior.
Your Windows delivery toolchain is ready. Next, you will package the inherited API into its first portable container image.
Run the API as a Container
The Part 1 FastAPI API still depends on one Windows Python environment. That dependency makes repeatable delivery harder.
In this step, you'll use Docker to package the API runtime with its project files in a container image. You'll prove that the image behaves the same way in local development and future automation.
In this step, get ready to:
- Define a reproducible container runtime for the inventory API.
- Build an image from a clean Docker build context.
- Run the image to confirm its tests and health endpoint work.
Create the container definition
A Dockerfile describes the runtime that Docker builds into an image. It gives the API the same Python base image, dependencies, files, port, and startup command each time.
- Create a file named Dockerfile inside the existing reliable-inventory-sync-api folder using the New File button in Visual Studio Code's Explorer sidebar.
- Paste this container definition into Dockerfile:
FROM python:3.14.7-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY main.py models.py database.py ./
COPY tests ./tests
EXPOSE 8000
CMD ["fastapi", "run", "main.py"]
What does this container definition do?
- The FROM python:3.14.7-slim instruction gives the image the same Python version used by the project.
- The WORKDIR /app instruction gives later commands a consistent directory inside the image.
- The first COPY instruction adds requirements.txt before Docker installs the pinned dependencies.
- The remaining COPY instructions add the inherited source files and test suite.
- The EXPOSE 8000 instruction documents the port used by the API.
- The CMD instruction starts the production FastAPI server from main.py.
- Save Dockerfile by pressing Ctrl+S.
- Check the Explorer sidebar for Dockerfile beside main.py.
That is the runtime boundary in place. Docker can now rebuild the API without relying on the active Windows virtual environment.
Dockerfile missing or named incorrectly?
Check that the filename is exactly Dockerfile with no extension. Make sure it sits directly inside reliable-inventory-sync-api beside requirements.txt.
Confirm that Visual Studio Code did not save the file as a text document with a hidden extension.
Help me check my Dockerfile location and filename.
✔️ Awesome, I've got everything!
Your saved Dockerfile now contains the complete runtime definition.
ⓧ I'd like to double check the full code
Compare your saved Dockerfile with this complete version.
FROM python:3.14.7-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY main.py models.py database.py ./
COPY tests ./tests
EXPOSE 8000
CMD ["fastapi", "run", "main.py"]
What should match?
Check every filename, path, blank line, and instruction against your file. The final command must start the API from main.py.
Build and test the image
Docker sends a build context containing the project files to its builder. A .dockerignore file removes local state that the image does not need.
- Create a file named .dockerignore inside reliable-inventory-sync-api using the New File button in the Explorer sidebar.
- Paste these build-context exclusions into .dockerignore:
.git
.github
.venv
__pycache__
.pytest_cache
*.pyc
inventory.db
What does this exclusion list protect?
- The Git entries keep repository metadata and future workflow files outside the image build context.
- The .venv entry keeps the Windows virtual environment out because the image installs its own dependencies.
- The cache and bytecode entries prevent generated Python files from being packaged.
- The inventory.db entry prevents local SQLite data from becoming part of the image.
- Save .dockerignore by pressing Ctrl+S.
- Check the Explorer sidebar for .dockerignore beside Dockerfile.
The build context now contains the source needed for the API without the local environment, caches, repository metadata, or database state.
Can't see .dockerignore?
Check that the filename begins with a full stop. Make sure the file has no extra extension.
Confirm that .dockerignore sits in the same reliable-inventory-sync-api folder as Dockerfile.
Help me fix my .dockerignore file.
✔️ Awesome, I've got everything!
Your saved .dockerignore now excludes every local-only path required by this project.
ⓧ I'd like to double check the full code
Compare your saved .dockerignore with this complete version.
.git
.github
.venv
__pycache__
.pytest_cache
*.pyc
inventory.db
What should match?
Check that every pattern appears on its own line. The final entry must exclude inventory.db.
The image definition and clean build context are ready. Building them now tests whether Docker can assemble the API independently.
- Switch back to the integrated PowerShell terminal from the previous step.
- Build the local image from the current project folder by running this command:
docker build -t reliable-inventory-sync-api:local .
What does this build command do?
- The docker build command processes Dockerfile.
- The -t flag assigns the image name reliable-inventory-sync-api with the tag local.
- The final full stop uses the current reliable-inventory-sync-api folder as the build context.
The build completes successfully when PowerShell returns to its prompt after Docker finishes every image layer.
Image build failed?
Confirm that the terminal is currently inside the reliable-inventory-sync-api folder. Check that Dockerfile and requirements.txt are visible in the Explorer sidebar.
If Docker cannot contact its engine, return to Docker Desktop from the previous step. Wait until it reports that the engine is running.
Help me diagnose my container image build.
A successful build proves that the runtime can be assembled. Running the inherited tests inside the image proves that its packaged code still preserves the Part 1 behavior.
Before you run the tests, do you expect the container to preserve the same four-test or five-test baseline as the Windows environment?
- Run the inherited pytest suite inside the image with this command:
docker run --rm reliable-inventory-sync-api:local pytest
Why test inside the image?
This command replaces the image's normal API startup command with pytest for one container run. The --rm flag removes that temporary container when the test process exits.
The result tests the exact files and dependencies that future automation uses. That closes the gap between host tests and packaged tests.
You'll see four passing tests. You'll see five if you completed the Part 1 Secret Mission.
Container tests did not pass?
Compare the failing test name with the baseline from the previous step. A different result usually means the image contains an unexpected file or dependency state.
Confirm that Dockerfile copies tests into the image. Recheck that it installs the inherited requirements.txt.
Help me compare my host and container test results.
Run the health endpoint
The API listens on port 8000 inside its container. Docker publishes that port on the Windows loopback address so your browser can reach the isolated process.
- Start the API as a detached container by running this command:
docker run --rm --name inventory-api-local -d -p 127.0.0.1:8000:8000 reliable-inventory-sync-api:local
What does this run command do?
- The --name inventory-api-local option gives the running container a predictable name.
- The -d flag keeps the API running in the background.
- The -p 127.0.0.1:8000:8000 option publishes the container's API port only on the local loopback address.
- The --rm flag removes the temporary container after it stops.
PowerShell returns to its prompt with the API still running in the background.
Container did not start?
Check that Docker Desktop is still running. Confirm that the image build and container test completed successfully.
Another local process may already be using port 8000. Stop that process before repeating the container command.
Help me start the local API container.
Before you open the endpoint, do you expect the packaged API to return the same health status as Part 1?
- Open the health endpoint in your browser by selecting http://127.0.0.1:8000/health.
You'll see {"status":"ok"} in the browser. That response proves the packaged API is serving requests from the local container.
Health endpoint unavailable?
Confirm that the browser address uses 127.0.0.1 with port 8000 and the /health path.
If the container exited, compare Dockerfile with the complete version above. Pay close attention to the final FastAPI command.
Help me restore the health endpoint.
- Stop the local demo container by running this command:
docker stop inventory-api-local
What happens when the container stops?
Docker stops the container named inventory-api-local. The earlier --rm option removes that temporary container after shutdown.
PowerShell returns to its prompt after Docker stops the demo container. Your reusable reliable-inventory-sync-api:local image remains available.
Container already stopped?
The API process may have exited before this command ran. The successful browser response still proves that the image served the health endpoint.
Help me confirm the demo container has stopped.
Your API now has a reproducible image that passes its inherited tests and serves its health endpoint. Next up, you'll create an independent GitHub Actions check that catches a deliberately broken change.
Make CI Catch a Broken Change
Your API now passes its tests inside a Docker image. The next risk is a change reaching main without anyone rerunning those tests.
GitHub Actions gives the repository an independent continuous integration check. The workflow tests the same container image that your release process will use.
You will challenge that guardrail with a deliberately incorrect assertion on a separate branch. The pull request decides whether the check deserves your trust.
In this step, get ready to:
- Publish the existing project to a public GitHub repository.
- Add a container workflow that tests pushes and pull requests.
- Open a pull request containing the planned test regression.
Publish the existing project
A remote repository gives Git a shared home for your commits. It also gives GitHub Actions a clean copy of the project to test.
- Return to GitHub in your browser.
- Select New repository.
- Enter reliable-inventory-sync-api in the Repository name field.
- Select Public as the repository visibility.
- Leave the repository initialization options unselected.
- Select Create repository.
- Copy the new repository's HTTPS URL.
- Record the copied URL here: https://github.com/YOUR-GITHUB-USERNAME/reliable-inventory-sync-api.git.
- Switch back to the PowerShell terminal in Visual Studio Code from earlier.
- Initialize the existing project as a repository by running these commands:
git init -b main
git add .
git commit -m "First commit"
What do these commands do?
- The first command creates the repository with main as its initial branch.
- The second command stages the existing API files for the first snapshot.
- The final command records that snapshot as the first commit.
- Confirm that the terminal reports a root commit containing the project files.
Did the first commit fail?
Git may need your author name or email before it can create a commit. Follow the configuration guidance printed in the terminal.
Make sure the PowerShell prompt points to the reliable-inventory-sync-api folder.
Ask for help with the first commit.
Your first authenticated HTTPS push may open browser-based GitHub authentication. This is Git Credential Manager confirming that this computer can write to your repository.
- Connect the local repository to GitHub by running these commands:
git remote add origin [[YOUR_REPO_URL="https://github.com/YOUR-GITHUB-USERNAME/reliable-inventory-sync-api.git"]]
git remote -v
git push -u origin main
How does the remote connection work?
- The remote named origin stores the URL of your GitHub repository.
- The remote check prints the saved fetch and push destinations.
- The push uploads main and sets its upstream branch.
- Complete the browser authentication flow if Git Credential Manager opens it.
- Return to the reliable-inventory-sync-api repository on GitHub.
- Refresh the repository page.
You should see the inherited API files beside Dockerfile and .dockerignore. Your portable API now has a public source-control home.
Did the push fail?
Confirm that the URL shown by the remote check matches the empty repository you created. A browser authentication window may be waiting behind Visual Studio Code.
Ask for help with the push.
Add the container CI workflow
A workflow turns repository events into repeatable jobs. This one builds the project image before running the inherited pytest suite inside it.
- Right-click the reliable-inventory-sync-api folder in Visual Studio Code's Explorer sidebar.
- Select New File.
- Enter .github/workflows/ci.yml in the file name field.
- Press Enter.
You should see ci.yml inside the new .github/workflows folders in the Explorer sidebar.
- Add the container workflow to .github/workflows/ci.yml by pasting this code:
name: Container CI
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
permissions:
contents: read
jobs:
test-container:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v7
- name: Build test image
run: docker build --tag reliable-inventory-sync-api:test .
- name: Run pytest inside the image
run: docker run --rm reliable-inventory-sync-api:test pytest
What does this workflow protect?
- The event rules run Container CI for pushes to main.
- The pull request rule tests proposed changes that target main.
- The permissions block grants read-only access to repository contents.
- The test-container job checks out the commit on a GitHub-hosted runner.
- The final two steps build reliable-inventory-sync-api:test before running pytest inside that image.
- Save .github/workflows/ci.yml by pressing Ctrl+S.
✔️ Awesome, I've got everything!
The saved workflow now covers pushes and pull requests that target main.
ⓧ I'd like to double check the full code
name: Container CI
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
permissions:
contents: read
jobs:
test-container:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v7
- name: Build test image
run: docker build --tag reliable-inventory-sync-api:test .
- name: Run pytest inside the image
run: docker run --rm reliable-inventory-sync-api:test pytest
- Publish the workflow on main by running these commands:
git add .github/workflows/ci.yml
git commit -m "Add container CI"
git push -u origin main
What happens after the push?
The commit stores the workflow beside the application code. Pushing that commit to main triggers its push rule.
GitHub Actions checks out the commit on ubuntu-latest. It builds the test image from the repository's Dockerfile.
Before you inspect the run, do you expect the same container test that passed locally to succeed on GitHub's runner?
- Return to the repository page on GitHub.
- Select the Actions tab.
- Select the Container CI workflow.
- Open the newest workflow run.
The first container build can take a few minutes while the runner downloads image layers. A completed run should show the test-container job in green.
Your workflow has passed its first independent test. That green run becomes the baseline for the regression challenge.
Is the first workflow run failing?
Open the failed job to identify whether checkout, image building, or pytest failed. Compare ci.yml with the full-file reference above.
Confirm that Dockerfile and the inherited test files are visible on main.
Ask for help with the workflow run.
Open a pull request with the regression
A branch isolates a proposed change from main. The pull request gives the workflow a candidate commit to evaluate before that change can be merged.
- Create the regression branch by running this command in the PowerShell terminal:
git switch -c ci-failure-demo
What does this branch command do?
The command creates ci-failure-demo from the current main commit. It also switches your working files to that branch.
The terminal should confirm that Git switched to the new branch.
- Select tests/test_main.py in Visual Studio Code's Explorer sidebar.
- Locate the duplicate-event replay assertion whose expected quantity is 4.
- Replace only that expected quantity with 5.
- Save tests/test_main.py by pressing Ctrl+S.
The branch now contains a deliberate mismatch between the replay behavior and the test expectation. The change remains isolated from main.
- Publish the regression branch by running these commands:
git add tests/test_main.py
git commit -m "Demonstrate CI failure"
git push -u origin ci-failure-demo
How does the branch reach GitHub?
- The first command stages only the changed test file.
- The commit records the deliberate assertion change on ci-failure-demo.
- The push publishes the branch without changing main.
- Return to the repository page on GitHub.
- Select the Pull requests tab.
- Select New pull request.
- Select main as the base branch.
- Select ci-failure-demo as the compare branch.
- Enter Demonstrate CI failure as the pull request title.
Before you create the pull request, do you expect the container check to accept the changed assertion?
- Select Create pull request.
The container check can take a few minutes while GitHub Actions builds the image. The pull request updates as each workflow step finishes.
You should see the Container CI check turn red. The independent guardrail has rejected the candidate change.
- Select the pull request's Checks tab.
- Open the failed test-container job.
- Open the failed Run pytest inside the image step.
The pytest log should show that the duplicate replay produced quantity 4 while the changed assertion expected 5. This failure proves that the pull request workflow executes the packaged tests.
Is the pull request check missing or green?
Confirm that the pull request uses main as its base. Confirm that ci-failure-demo is its compare branch.
Open the pull request's changed files to confirm that tests/test_main.py contains the expected quantity of 5.
Ask for help with the missing failure.
Your pull request has produced the red evidence that a broken change cannot pass unnoticed. Next, you will restore the correct expectation and merge only after the same container check turns green.
Repair the Regression and Merge Green
Your deliberate regression has proved that GitHub Actions can stop a faulty pull request. The failed pytest log now points to the expectation that caused the check to fail.
This step turns that red evidence into a safe merge. A green rerun becomes your permission to merge.
In this step, get ready to:
- Use the failing pytest log to identify the incorrect expectation.
- Restore the expected duplicate replay quantity to 4.
- Complete the green merge workflow for main.
Read the failure and restore the assertion
A failing assertion compares the value produced by the API with the value expected by the test. The useful assertion line can sit among many build messages.
Finding the relevant line can feel fiddly. Follow the failed job down to the pytest step to isolate it.
- Return to the pull request from the previous step.
- Select the red Container CI check in the merge box.
- Select test-container from the workflow run.
- Expand Run pytest inside the image to read the failed test output.
The log shows an actual replay quantity of 4. The assertion incorrectly expects 5.
Why is 4 correct?
Idempotency means replaying the same inventory event leaves the recorded quantity unchanged. The first event produced the quantity 4. The duplicate must preserve that value.
- Switch back to Visual Studio Code.
- Select tests/test_main.py from the Explorer sidebar.
- Find the duplicate replay assertion whose expected quantity is 5.
- Replace the expected quantity of 5 with 4.
- Save tests/test_main.py by pressing Ctrl+S.
Push the repair and watch CI recover
A new commit on the same branch updates the existing pull request. GitHub reruns its continuous integration checks against that commit.
- Use your established Git workflow to stage only tests/test_main.py.
- Commit the staged repair with the message Restore duplicate replay expectation.
- Push the commit to ci-failure-demo.
GitHub updates the pull request automatically. A new Container CI run starts for the repaired commit.
The rerun rebuilds the container image before it runs the tests. A short wait here is normal.
Before you inspect the result, what do you expect Container CI to show after the expected quantity is restored? Hold that prediction for the check.
- Return to the updated pull request.
- Select the Checks tab.
- Select the latest Container CI run.
You should see the pull-request check turn green. Pytest reports four passing tests or five if you completed the Part 1 Secret Mission.
That repair loop is complete. The same check that rejected the regression now proves the corrected branch passes.
Is the pull request still red?
- Confirm that tests/test_main.py expects a duplicate replay quantity of 4.
- Confirm that the repair commit appears on ci-failure-demo.
- Select the newest workflow run to avoid reading the earlier deliberate failure.
Help me diagnose the repaired Container CI check.
Merge the green change and clean up
A green pull-request check certifies the repaired branch revision. Merging that revision triggers the workflow for the resulting main commit.
Deleting ci-failure-demo removes the temporary remote branch after the merge. The repaired commit remains on main.
- Return to the pull request conversation page.
- Confirm that the latest Container CI check is green.
- Click Merge pull request at the bottom of the pull request.
- Click Confirm merge.
- Click Delete branch.
The pull request now shows as merged. The remote ci-failure-demo branch is removed.
Your local Git repository still points to the feature branch. Switch it to the merged branch before removing the local copy.
- Update local main and remove local ci-failure-demo by running these commands in the Visual Studio Code terminal:
git switch main
git pull
git branch -d ci-failure-demo
What do these commands do?
- git switch main moves your local workspace onto the stable branch.
- git pull retrieves the merged commit from the configured upstream.
- git branch -d ci-failure-demo safely deletes the merged local branch.
- Confirm that PowerShell reports the deletion of ci-failure-demo.
Could not delete the local branch?
Confirm that the pull request finished merging before you update local main. Git protects branch work that it cannot confirm is merged.
Help me remove the merged local branch safely.
Before the final check, what do you expect the workflow for the merged main commit to report? Hold that prediction for the result.
- Return to the reliable-inventory-sync-api repository on GitHub.
- Click Actions under the repository name.
- Select the latest Container CI run for main.
You should see the workflow finish successfully. Its pytest step reports four passing tests or five with the Part 1 Secret Mission.
You have closed the delivery loop. The regression was rejected before the repaired commit reached a green main branch.
Is the main workflow missing or failing?
- Confirm that the pull request shows as merged into main.
- Confirm that you selected the workflow run for the merge commit.
- Open Run pytest inside the image to identify any failing test in the latest run.
Help me diagnose the merged main workflow.
Your repaired main branch now has independent test evidence behind it. Next, you will turn that tested commit into a versioned container release.
Publish the v2.0.0 Container Release
Your repaired main branch now has green container CI. That check protects the source, but consumers still need a versioned container image they can pull.
This step adds a GitHub Actions workflow that responds to published releases. It tests the release candidate before publishing v2.0.0 to GitHub Container Registry.
In this step, get ready to:
- Create a release workflow that tests each release candidate before publication.
- Publish release v2.0.0 from main and make its container package public.
- Pull the public image and prove that its health endpoint works locally.
Add the release workflow
A release workflow turns a published GitHub release into a repeatable delivery event. Its first responsibility is to rebuild the candidate and run the inherited tests before registry authentication begins.
- In the Explorer sidebar in Visual Studio Code, expand the .github/workflows folder.
- Right-click the .github/workflows folder.
- Select New File.
- Name the file release.yml.
- Define the release trigger and test gate by adding this first section to release.yml.
name: Release Container
on:
release:
types: [published]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Check out repository
uses: actions/checkout@v7
- name: Build release candidate
run: docker build --tag reliable-inventory-sync-api:release-candidate .
- name: Test release candidate
run: docker run --rm reliable-inventory-sync-api:release-candidate pytest
How does the release gate work?
- The published release event starts the workflow only after you publish a GitHub release.
- The publish job receives read access to repository contents and write access to packages.
- The release candidate is built from the tagged repository state.
- Pytest must finish successfully before any registry login step can run.
- Save release.yml.
- Select the Source Control icon in the left activity bar.
You should see release.yml listed as a new file. Its diff shows the release trigger and test gate you just added.
Does the workflow section look misaligned?
YAML uses indentation to show which settings belong together. Compare the spacing under release, publish, and steps with the code block above.
Ask for help if the editor highlights the workflow structure. Help me check my release workflow indentation
The tests now guard the release candidate. The next section authenticates the workflow with the repository's temporary token so it can publish the approved image.
- Add the registry login step below the Test release candidate step in release.yml.
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
Why use the workflow token?
- The workflow signs in to ghcr.io as the account that triggered the release.
- The GITHUB_TOKEN value is supplied by GitHub Actions for this workflow run.
- The job's packages: write permission limits the token to the access needed for publication.
- Save release.yml.
- Return to the Source Control panel.
The updated diff should now show the login step immediately after the release candidate test. This order prevents registry authentication when the tests fail.
Is the login step outside the job?
Make sure the new - name line aligns with the existing build and test step names. The with values belong beneath docker/login-action@v4.
Use this prompt if you want help checking the nesting. Help me place the registry login step
A registry needs a valid image path and release tag. The metadata step derives both from the repository name and the published Git tag.
- Add the metadata step below the registry login step in release.yml.
- name: Derive release tags and labels
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=tag
What does the metadata step produce?
- The images input combines the registry hostname with the current repository name.
- The type=ref,event=tag rule converts the published Git tag into the container image tag.
- The meta identifier makes the generated tags and labels available to the publishing step.
- Save release.yml.
- Review the latest change in the Source Control panel.
You should see the metadata step nested beneath the login step. Its tag rule should sit two spaces deeper than tags.
Is the tag rule highlighted?
Keep the vertical bar after tags: and place the tag rule on the next line. Indent that rule beneath the tags key.
Use this prompt if the YAML editor still marks the block as invalid. Help me fix the metadata tag block
The workflow now knows where the image belongs. Its final step uses the generated metadata to build and push the release artifact.
- Add the publishing step below the metadata step in release.yml.
- name: Build and push release image
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
How is the release image published?
- The current repository directory becomes the Docker build context.
- The push: true setting sends the completed image to the registry.
- The metadata outputs apply the release tag and generated labels to the published image.
- Save release.yml.
- Review the complete file in the Explorer sidebar.
Your workflow should now end with the Build and push release image step. The test step remains above every registry operation.
Is the publishing step missing metadata?
Check that the metadata step still has id: meta. The publishing step uses that identifier in both output references.
Ask for help if either reference is highlighted. Help me connect the metadata outputs
✔️ Awesome, I've got everything!
Your complete release.yml workflow is saved. It is ready to commit to main.
ⓧ I'd like to double check the full code
Compare your complete release.yml file with this reference.
name: Release Container
on:
release:
types: [published]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Check out repository
uses: actions/checkout@v7
- name: Build release candidate
run: docker build --tag reliable-inventory-sync-api:release-candidate .
- name: Test release candidate
run: docker run --rm reliable-inventory-sync-api:release-candidate pytest
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Derive release tags and labels
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=tag
- name: Build and push release image
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- Select the Source Control icon in the left activity bar.
- Stage release.yml using the plus button beside the file.
- Enter Add release workflow in the commit message field.
- Select Commit.
- Select Sync Changes.
The release workflow is now stored on main. GitHub can use it when you publish the release.
Publish the v2.0.0 release
A GitHub release gives the tested commit a stable version name. Publishing v2.0.0 triggers the workflow you just added.
- Return to the public reliable-inventory-sync-api repository in your browser.
- Select Releases.
- Select Draft a new release.
- Select Choose a tag.
- Enter v2.0.0.
- Select Create new tag.
- Confirm that main is the tag target.
The tag now points at the repaired commit with green container CI. The release description records what this version delivers.
- Enter Reliable inventory API v2.0.0 in the Release title field.
- Enter Tested container delivery for the reliable inventory sync API. in the description field.
- Select Publish release.
The first release build can take a few minutes while GitHub builds the image layers. A quiet workflow page during that time does not mean the job has stalled.
- Select Actions in the repository navigation.
- Select the Release Container workflow.
- Open the run triggered by release v2.0.0.
- Wait for the publish job to turn green.
You should see successful steps for building the release candidate, running pytest, authenticating, deriving metadata, and pushing the image. The green run proves the published artifact passed the same container test gate.
Did the release workflow fail?
Open the first failed step in the workflow run. Check the matching section of release.yml for indentation or copied-value differences.
Use the failed step name when asking for help. Help me debug my release workflow
The successful workflow creates the package with private visibility. Making it public lets another developer pull the release without registry credentials.
Public visibility cannot be reversed for this package. That permanence is why GitHub asks you to confirm the change before it applies.
- Open the reliable-inventory-sync-api container package from the repository's Packages section.
- Select Package settings.
- Scroll to Danger Zone.
- Select Change visibility.
- Select Public.
- Complete the confirmation shown by GitHub.
The package page should now show public visibility. Its v2.0.0 image can be pulled anonymously.
- Record the GitHub username shown in the package pull path here: YOUR-GITHUB-USERNAME.
Prove the public release works
A public registry package becomes useful when someone can download it and run it away from the workflow. You will use Docker to repeat that consumer experience on your Windows computer.
- Return to the PowerShell terminal in Visual Studio Code.
- Pull the public v2.0.0 image by running this command:
docker pull ghcr.io/[[YOUR_GITHUB_USERNAME="YOUR-GITHUB-USERNAME"]]/reliable-inventory-sync-api:v2.0.0
What does this pull prove?
Docker retrieves the versioned image from the public package instead of rebuilding from your local files. A successful pull proves that the registry can deliver the release to a consumer.
The command should finish after listing the downloaded image layers. The local image now carries the public v2.0.0 tag.
Could Docker not pull the image?
Check that the package page shows public visibility. Confirm that the username in the image path matches the package owner exactly.
Ask for help if the package path still fails. Help me troubleshoot the public image pull
- Start the released API on loopback port 8000 by running this command:
docker run --rm --name inventory-api-release -d -p 127.0.0.1:8000:8000 ghcr.io/[[YOUR_GITHUB_USERNAME="YOUR-GITHUB-USERNAME"]]/reliable-inventory-sync-api:v2.0.0
How does this run the release?
- The container receives the name inventory-api-release so you can identify it during cleanup.
- The loopback address limits the published port to your local computer.
- Detached mode keeps the API running while you test it in the browser.
- The container removes itself after it stops.
PowerShell should print the new container identifier. That output confirms the released image started as a local container.
Did the release container stop immediately?
Confirm that Docker Desktop is still running. Check that another container is not already using local port 8000.
Use this prompt to inspect the startup problem. Help me debug the release container
Before you open the endpoint, consider whether its response should change because the image came from the registry.
- Switch back to the browser tab you used for GitHub.
- Enter http://127.0.0.1:8000/health in the address bar.
You should see {"status":"ok"} in the browser. The response matches the local container demonstration because the registry release contains the same tested API.
That is the release proven from source to registry. Your public v2.0.0 image passes its test gate and serves the expected health response.
Secret mission
Attest the Container Build
Your public container image is ready to run. Add an artifact attestation that links its digest to the GitHub Actions workflow that built it, then publish v2.0.1 with recorded build provenance.
Clean Up Your Resources
Clean Up Your Resources
The public reliable-inventory-sync-api repository uses free standard runners from GitHub. Choose whether to keep the currently free GitHub Container Registry package, pause local processes, or delete every project resource.
Resources you used:
- A public repository named reliable-inventory-sync-api, including its releases and GitHub Actions run history.
- A public container package at ghcr.io/YOUR-GITHUB-USERNAME/reliable-inventory-sync-api with release tags v2.0.0 and v2.0.1.
- An artifact attestation attached to the container digest from release v2.0.1.
- Local images reliable-inventory-sync-api:local and ghcr.io/YOUR-GITHUB-USERNAME/reliable-inventory-sync-api:v2.0.0 in Docker Desktop.
- Local demo containers named inventory-api-local and inventory-api-release.
- The local reliable-inventory-sync-api project folder containing the API and delivery workflows.
Keep everything running
No action needed. Choose this if you want the public releases to stay available for demonstrations or future development.
- Keep the public reliable-inventory-sync-api repository available on GitHub.
- Keep the public container package available for anonymous pulls.
- Keep the local project folder available for future changes.
- Review Docker's subscription terms if you move this work to an employer-owned machine or a larger enterprise.
Pause - I'll come back to this later
Shut down local processes to free system resources. Your local images and public releases stay available for the next demonstration.
- Check Docker Desktop to see whether inventory-api-release is running.
- Stop inventory-api-release if it is running by using this command:
docker stop inventory-api-release
What Does This Command Do?
Docker stops the release container while leaving the downloaded image available for another run.
If the container was running, you'll see inventory-api-release printed in Windows PowerShell.
Container Already Stopped?
If Windows PowerShell reports that the container is unavailable, Docker has nothing left to stop.
Help me check whether the release container is still running.
- Exit Docker Desktop from the Windows notification area.
Your local processes are now quiet. The repository, package, attestation, images, and project files remain available.
Delete - I don't want to use this again
Deleting these resources is permanent, so preserve any work you want to keep first. Your public releases and local project files disappear after this cleanup.
Remove Local Docker Assets:
- Return to Docker Desktop.
- Check whether inventory-api-release is running.
- Stop inventory-api-release if it is running.
- Remove any remaining container named inventory-api-local.
- Remove any remaining container named inventory-api-release.
- Remove reliable-inventory-sync-api:local from the local image list.
- Remove ghcr.io/YOUR-GITHUB-USERNAME/reliable-inventory-sync-api:v2.0.0 from the local image list.
You should no longer see either project image tag or either container name in Docker Desktop.
Remove the Published Package:
- In GitHub, go to the package page for ghcr.io/YOUR-GITHUB-USERNAME/reliable-inventory-sync-api.
- Select Package settings.
- In Danger Zone, use the deletion control to permanently remove the package.
- Confirm that the public package page is no longer available.
Remove the Public Repository:
- Return to the reliable-inventory-sync-api repository on GitHub.
- Open the repository settings page.
- Scroll to the repository deletion area.
- Permanently delete the reliable-inventory-sync-api repository.
- Confirm that the repository page no longer shows the project.
Remove the Local Project Folder:
Finding the folder can be fiddly because its Windows path depends on where Part 1 was saved. Visual Studio Code can take you directly to its current location.
- In Visual Studio Code, use the Explorer sidebar to reveal reliable-inventory-sync-api in File Explorer.
- Close Visual Studio Code.
- In File Explorer, delete the reliable-inventory-sync-api folder from its current location.
- Empty the Windows Recycle Bin to remove the folder completely.
- Confirm that reliable-inventory-sync-api no longer appears in its parent folder.
Cleanup complete. The local container assets and public release resources are now gone.
Nice Work!
Nice Work!
You made it through Part 2! Your inherited FastAPI service now ships through a tested Docker release pipeline powered by GitHub Actions.
You've learned how to:
- Package the inherited API as a reproducible Docker image that runs the complete pytest suite inside the container.
- Use a GitHub Actions pull-request check to block a deliberate inventory regression before merge.
- Publish public release v2.0.0 to GitHub Container Registry. Prove the released image works by receiving {"status":"ok"} from its /health endpoint.
- Secret Mission: Add an artifact attestation to release v2.0.1. Bind the pushed container digest to the GitHub Actions build that produced it.
Ready to quiz yourself?