NAS_HOST can be either 'host' or 'host:port'. The previous workflow
passed the whole value as the SSH target, which fails with
'Could not resolve hostname 192.168.1.21:222' when the port is included.
Parse the secret into NAS_SSH_HOST and NAS_SSH_PORT and pass them
explicitly to ssh.
act_runner v0.6.1 does not propagate the runner's network namespace
to job containers, so the job can never resolve 'portainer' or reach
'192.168.1.30:9000'. After trying every combination of
container.network_mode / container.network / options in the runner
config, the only reliable solution is to skip the in-cluster Portainer
API call and do the deploy via SSH directly on the NAS.
- New secrets: NAS_SSH_KEY, NAS_HOST, NAS_USER.
- The new 'Redeploy stack on Portainer via SSH' step writes the SSH key
to ~/.ssh, then pipes docker-compose.prod.yml to
'docker compose -p davidaragon-portfolio -f - up -d' over SSH.
- The old Portainer-API step is removed.
Smoke-test at the end hits the public port on the host (3001) and
logs the HTTP code; we don't fail the job if the public proxy
upstream isn't reachable from the runner's network.
Drop the secret indirection and just use the service name directly.
All services share the portainer_default Docker network so 'portainer'
resolves to Portainer from any container in that network.
All services (Gitea, the 3 runners and Portainer itself) are in the
same Docker network 'portainer_default', so the service name 'portainer'
resolves directly from inside any container in that network. The previous
auto-discovery dance with IPs and routes is no longer needed.
- Set the PORTAINER_URL secret to 'http://portainer:9000'.
- Replace the entire 'Step 0' auto-discovery with a simple check that
the configured URL is reachable.
- Drop the debug Step 0a that was added during troubleshooting.
The runner's job container can now reach Portainer the same way the
runner itself does — by name.
- Drop 'set -euo pipefail' from the redeploy step so the auto-discovery
loop and the diagnostic echos can run to completion even if a probe
fails or a command returns empty output.
- On failure to find Portainer, also print the container's hostname,
hostname -I, and ip route to stderr. This is what we'll need if the
host-net job container approach doesn't work and we need to debug
further.
Adds Step 0a that prints hostname -I, ip route, and a direct curl to
192.168.1.30:9000. This is to verify whether the runner external (in
network_mode: host) is actually propagating host networking to the job
containers it spawns. Symptom: deploy step fails with 'Failed to
connect to 192.168.1.30 port 9000' even from the external runner.
To be removed once we know.
The act_runner on the QNAP creates an ephemeral docker bridge per job.
That network can't always reach the canonical PORTAINER_URL
(http://192.168.1.30:9000) — first deploy run failed with
'Failed to connect to 192.168.1.30 port 9000 after 130317 ms'.
New step 0 probes several candidate URLs (container's own IPs, default
gateways, then the secret value as fallback) against /api/status and
uses the first one that returns 200. The reachable host is then reused
for the DELETE+CREATE calls and the smoke-test on :3001.
Documents the current pipeline anatomy, the metrics to capture on the
first real run, hypothesised bottlenecks, and ranked optimisation
proposals. Placeholders for real numbers; to be filled in after the
first deploy lands.
Companion to the Portainer redeploy step in .gitea/workflows/ci-cd.yaml
on the same branch.
- Build pushes both :latest and :0.0.1 tags so the redeploy step uses the
pinned version regardless of how the runner happened to be tagged before.
- Add a 4th step 'Redeploy stack on Portainer' that runs ONLY when the
workflow is manually dispatched with deploy=true.
- Step does idempotent DELETE+CREATE on the existing stack via Portainer REST
(PUT-edit would require more plumbing for a single-service stack).
- Smoke-tests http://<host>:3001/ after the stack recreates; logs the code
but does not hard-fail the job (network-proxy reachable via NPM upstream).
Required new secrets (in addition to existing DOCKER_USERNAME/DOCKER_PASSWORD):
- PORTAINER_URL e.g. http://192.168.1.30:9000
- PORTAINER_TOKEN API access token from a Portainer user
- PORTAINER_ENDPOINT_ID numeric endpoint ID (usually 1)
- PORTAINER_STACK_ID numeric stack ID to delete before recreating
Apply the same concurrency pattern used in the python-project-template:
- cancel-in-progress: false prevents the shared act_runner from
cancelling in-progress steps when a new push lands
- pull_request trigger ensures PRs are validated before merge
- workflow_dispatch allows manual re-runs
- Homepage with hero, featured projects, latest posts sections
- Content collections config (blog + projects schemas)
- Date formatting and reading time utilities
- Sample blog post and project for validation
- Add base layouts (BaseLayout, BlogLayout, ProjectLayout)
- Add UI components (Header, Footer, Navigation, Card, Tag)
- Add About page with personal story
- Add Projects pages (index, detail)
- Add homepage content
- Add SEO files (robots.txt, webmanifest)
Work was done by agents in isolated workspaces.
Consolidated into main repo for proper git tracking.