Org-wide reusable GitHub Actions workflows for minhdqdev-org.
Builds and pushes a Docker image to Harbor, then sends a success notification to NotiHub. Branch is detected at runtime to apply the correct tag strategy:
| Branch | Tag produced | Environment | ArgoCD strategy |
|---|---|---|---|
develop |
dev-<short_sha> |
DEV | latest, regexp:^dev-.*$ |
rc/<version> |
<version>-rc.<commit_count> |
UAT | semver, regexp:^.*-rc\.[0-9]+$ |
main / master |
<version> + latest |
PROD | semver |
Version is resolved from package.json, pyproject.toml, or Cargo.toml. Falls back to
the short commit SHA if none are present.
Name RC branches after the version they release — rc/0.1.120, not rc/my-feature. The
workflow triggers on rc/** so any name builds, but the branch name is how anyone reading the
branch list knows which release is in UAT.
<commit_count> is git rev-list --count HEAD, not github.run_number: it is per-branch
monotonic with no gaps from unrelated branches. This is why the checkout step sets
fetch-depth: 0 — under the default shallow clone the count is always 1, every RC builds
<version>-rc.1, and ArgoCD Image Updater sees no newer tag and silently stops deploying.
It also means the version must be bumped for each new RC of the same release. Two RC builds at the same version and commit count collide on the tag; the second overwrites the first and nothing deploys.
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [develop, main, "rc/**"]
jobs:
ci:
uses: minhdqdev-org/github-actions-workflows/.github/workflows/ci.yml@main| Input | Default | Description |
|---|---|---|
dockerfile |
./Dockerfile |
Path to the Dockerfile |
dockerfile_target |
"" |
Build target for multi-stage Dockerfiles |
jobs:
ci:
uses: minhdqdev-org/github-actions-workflows/.github/workflows/ci.yml@main
with:
dockerfile: ./deploy/dockerfilejobs:
ci:
uses: minhdqdev-org/github-actions-workflows/.github/workflows/ci.yml@main
with:
dockerfile_target: productionjobs:
test:
if: github.ref == 'refs/heads/main'
runs-on: [self-hosted, minhdqdev-org, manual]
steps:
- uses: actions/checkout@v4
- run: bun install --frozen-lockfile
- run: bun test --coverage
ci:
needs: [test]
if: always() && (needs.test.result == 'success' || needs.test.result == 'skipped')
uses: minhdqdev-org/github-actions-workflows/.github/workflows/ci.yml@mainRuns on a repo's pull_request trigger. Builds and pushes nothing, so it is safe on every PR.
ci.yml runs after code lands on a branch; this runs before.
| Job | Tool | Scope |
|---|---|---|
| Secret scan | gitleaks | commits the PR adds (base..head) |
| SAST | Semgrep | findings new relative to the PR base commit |
| Dependency scan | Trivy fs |
whole lockfile (uv.lock, bun.lock, package-lock.json, pubspec.lock, ...) |
| Dockerfile lint | hadolint | skipped when there is no Dockerfile |
| Checks | caller-supplied | install_command, then check_command and test_command |
| PR check | — | one job to point a required status check at |
Findings are scoped to the change where possible, so a brownfield repo does not start red.
The dependency scan cannot be scoped to the diff, because an advisory belongs to the dependency.
Set dependency_scan_fail: false to report without blocking while a backlog is cleared, then flip it back.
# .github/workflows/pr-check.yml
name: PR Check
on:
pull_request:
branches: [develop, main]
jobs:
pr-check:
uses: minhdqdev-org/github-actions-workflows/.github/workflows/pr-check.yml@main
with:
install_command: uv sync --frozen --extra dev
check_command: uv run ruff check src/| Input | Default | Description |
|---|---|---|
secret_scan / sast / dependency_scan / dockerfile_lint |
true |
Enable each job |
sast_fail / dependency_scan_fail / checks_fail |
true |
false makes that job report-only |
dependency_severities |
CRITICAL,HIGH |
Severities that count as findings |
semgrep_config |
p/default |
Semgrep ruleset |
dockerfile |
./Dockerfile |
Dockerfile to lint |
working_directory |
. |
Where the install, check and test commands run |
install_command |
"" |
Empty skips the Checks job |
check_command / test_command |
"" |
Empty skips that step |
runs_on |
["self-hosted","minhdqdev-org","manual"] |
JSON array of runner labels |
test_command must not need services the runner does not have (for example a database).
Nothing from the github or inputs contexts may appear inside a run: body in this repo.
${{ }} is substituted as raw text before bash parses the script, so a branch name becomes code.
Pass the value through env: instead.
- Runner:
[self-hosted, minhdqdev-org, manual]with Harbor credentials injected viaenvFrom(HARBOR_USERNAME,HARBOR_PASSWORD) on the runner pod. - Registry:
harbor.internal.minhdq.dev/minhdqdev/<repo-name> - Build cache: stored at
harbor.internal.minhdq.dev/minhdqdev/<repo-name>:buildcache