Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/workflows/promote-to-prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# GitHub Actions 탭에서 버튼 한 번으로 상용 배포를 트리거하기 위한 워크플로.
# main의 다음 semver 태그(v1.2.3 형태)를 자동으로 계산해 push하고,
# 그 태그 push가 prod-cd.yml의 트리거(on.push.tags)를 실행시켜 상용 배포로 이어진다.
name: Promote to Production

on:
workflow_dispatch:
inputs:
bump:
description: "버전 증가 단위"
required: true
default: "patch"
type: choice
options:
- patch
- minor
- major

permissions:
contents: write

jobs:
tag:
runs-on: ubuntu-latest
Comment on lines +22 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

file=".github/workflows/promote-to-prod.yml"
printf '%s\n' '--- workflow ---'
cat -n "$file"

printf '%s\n' '--- related workflow concurrency and tag references ---'
rg -n --glob '.github/workflows/**' 'concurrency:|cancel-in-progress|promote-to-prod|git tag|GITHUB_REF|github\.ref' .github/workflows || true

Repository: Runnect/Runnect-Spring-Boot-Server

Length of output: 2847


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

text = Path(".github/workflows/promote-to-prod.yml").read_text()

assert re.search(r"(?m)^on:\s*$", text)
assert re.search(r"(?m)^\s+workflow_dispatch:\s*$", text)
assert not re.search(r"(?m)^concurrency:", text)

tag_lines = re.search(
    r"(?ms)^\s+LATEST=\$\(git tag --list 'v\*'.*?^\s+NEW_TAG=\"v\$\{MAJOR\}\.\$\{MINOR\}\.\$\{PATCH\}\"",
    text,
)
assert tag_lines, "expected latest-tag and NEW_TAG logic was not found"

def next_tag(latest, bump):
    major, minor, patch = map(int, latest.removeprefix("v").split("."))
    if bump == "major":
        major, minor, patch = major + 1, 0, 0
    elif bump == "minor":
        minor, patch = minor + 1, 0
    elif bump == "patch":
        patch += 1
    else:
        raise ValueError(bump)
    return f"v{major}.{minor}.{patch}"

for bump in ("major", "minor", "patch"):
    latest = "v1.2.3"
    first_read = next_tag(latest, bump)
    second_read = next_tag(latest, bump)
    assert first_read == second_read
    print(f"{bump}: both runs read {latest!r} -> {first_read!r}; the second push targets the same tag")
PY

Repository: Runnect/Runnect-Spring-Boot-Server

Length of output: 415


Serialize production promotion runs.

Two manual runs can calculate the same next tag. The second git push then fails because the tag already exists. Add a workflow-level concurrency group with cancel-in-progress: false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/promote-to-prod.yml around lines 22 - 24, Add
workflow-level concurrency configuration to promote-to-prod.yml, using a shared
group for production promotion runs and setting cancel-in-progress to false so
queued manual runs execute sequentially. Keep the existing tag job unchanged.

steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
Comment on lines +26 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/promote-to-prod.yml
printf '%s\n' '--- repository action references and runner versions ---'
rg -n --glob '*.yml' --glob '*.yaml' 'actions/checkout@|runs-on:|actionlint' .
printf '%s\n' '--- available actionlint ---'
if command -v actionlint >/dev/null 2>&1; then actionlint -version; else echo 'actionlint not installed'; fi

Repository: Runnect/Runnect-Spring-Boot-Server

Length of output: 2640


🌐 Web query:

GitHub actions/checkout latest release v6.0.2 runner requirements Node.js 24

💡 Result:

The actions/checkout version 6.0.2 uses the Node.js 24 runtime [1][2]. To support this runtime, a minimum GitHub Actions Runner version of v2.327.1 or later is required [1][2]. Additionally, if you use the persist-credentials feature in workflows involving Docker container actions, a minimum runner version of v2.329.0 or later is required to properly access the credentials stored in the runner's temporary directory [3][1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- checkout releases ---'
curl -fsSL https://api.github.com/repos/actions/checkout/releases/latest |
  jq '{tag_name, name, published_at, html_url}'
printf '%s\n' '--- checkout v3 and v6 metadata ---'
for tag in v3 v6; do
  printf '%s\n' "[$tag]"
  curl -fsSL "https://api.github.com/repos/actions/checkout/git/ref/tags/$tag" |
    jq '{ref, object}'
done
printf '%s\n' '--- repository policy files mentioning action pinning ---'
rg -n -i 'pin(ned|ning)?|commit sha|full[- ]length sha|actions/checkout|github actions' \
  .github README.md CONTRIBUTING.md SECURITY.md 2>/dev/null || true
printf '%s\n' '--- actionlint availability ---'
if command -v actionlint >/dev/null 2>&1; then
  actionlint -version
else
  echo 'actionlint not installed'
fi

Repository: Runnect/Runnect-Spring-Boot-Server

Length of output: 1137


🏁 Script executed:

#!/bin/bash
set +e
printf '%s\n' '--- actionlint target workflow ---'
actionlint .github/workflows/promote-to-prod.yml
printf '%s\n' '--- actionlint all workflows ---'
actionlint .github/workflows
printf '%s\n' '--- checkout tags from the upstream repository ---'
git ls-remote --tags --refs https://github.com/actions/checkout.git 'v[0-9]*' |
  tail -n 20

Repository: Runnect/Runnect-Spring-Boot-Server

Length of output: 1778


🌐 Web query:

official actions/checkout v7.0.1 README runner requirements Node.js 24 GitHub Actions

💡 Result:

The official actions/checkout v7.0.1 action requires the GitHub Actions runner to be at version v2.327.1 or later to support the Node.js 24 runtime [1][2][3]. This requirement is explicitly stated in the action's documentation as a result of its migration to Node.js 24 [1][2]. While the action utilizes Node.js 24 for execution, the runner-level requirement ensures that the underlying infrastructure is compatible with this runtime version [4].

Citations:


Update actions/checkout@v3 to a supported major.

Use v7.0.1 only when the runner is version v2.327.1 or later. Pin the reviewed commit if repository policy requires SHA pins.

🧰 Tools
🪛 actionlint (1.7.12)

[error] 26-26: the runner of "actions/checkout@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🪛 zizmor (1.29.0)

[warning] 26-28: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/promote-to-prod.yml around lines 26 - 28, Update the
actions/checkout step in the promote-to-prod workflow from v3 to v7.0.1,
ensuring it runs only on runners at v2.327.1 or later. Preserve fetch-depth: 0
and pin the reviewed commit instead of the tag if repository policy requires
SHA-pinned actions.

Source: Linters/SAST tools


🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/promote-to-prod.yml
printf '%s\n' '--- related workflow references ---'
rg -n --hidden --glob '!node_modules' 'promote-to-prod|workflow_dispatch|actions/checkout@' .github README.md 2>/dev/null || true

Repository: Runnect/Runnect-Spring-Boot-Server

Length of output: 3222


🌐 Web query:

GitHub Actions workflow_dispatch selected branch workflow file permissions github.ref actions/checkout default ref documentation

💡 Result:

To trigger a workflow using the workflow_dispatch event, the workflow file must exist on the repository's default branch [1][2][3]. However, once triggered, you can select any branch or tag to run the workflow against via the GitHub UI, CLI, or API [1][4][5]. Key points regarding branch selection and permissions: 1. Workflow File Availability: The workflow file itself must be present in the default branch to be discoverable and triggerable [1][2]. If you want to run a version of a workflow that exists only on a feature branch, it must first be merged into the default branch [6]. 2. Selecting the Ref: - GitHub UI: When manually triggering the workflow, a dropdown menu allows you to select the desired branch or tag [1][4]. - GitHub CLI: Use the --ref flag (e.g., gh workflow run WORKFLOW --ref BRANCH) [1][4]. - API: Provide the ref parameter in the request body [5]. 3. github.ref Context: The github.ref context variable represents the branch or tag that was selected and triggered the run [7][8]. For a branch, it is returned in the format refs/heads/<branch_name>, and for a tag, it is refs/tags/<tag_name> [7][8]. 4. actions/checkout Behavior: When using the actions/checkout action, if you do not specify a ref, it defaults to the branch or tag that triggered the workflow run (the value of github.ref) [9][10]. This ensures that the code checked out matches the branch/tag you selected in the UI/CLI/API [9][10]. 5. Permissions: Users must have write access to the repository to trigger a workflow_dispatch event [1]. No additional special permissions are required for the workflow itself to access different branches, provided the user has sufficient repository access to trigger it [1]. In summary, while the workflow definition is pinned to the default branch, the execution context (code version) is determined by the ref you select at runtime, which is automatically respected by the default behavior of actions/checkout [1][9].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- production workflow ---'
cat -n .github/workflows/prod-cd.yml
printf '%s\n' '--- deployment documentation ---'
sed -n '160,195p' README.md

Repository: Runnect/Runnect-Spring-Boot-Server

Length of output: 5298


Restrict promotion to main.

workflow_dispatch allows a branch or tag selection. Without ref, actions/checkout checks out that selected ref. The subsequent git tag and push can therefore trigger production deployment from a non-main commit. Add a main guard and check out main explicitly.

Suggested fix
 jobs:
   tag:
+    if: github.ref == 'refs/heads/main'
     runs-on: ubuntu-latest
@@
         with:
           fetch-depth: 0
+          ref: main
🧰 Tools
🪛 actionlint (1.7.12)

[error] 26-26: the runner of "actions/checkout@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🪛 zizmor (1.29.0)

[warning] 26-28: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/promote-to-prod.yml around lines 26 - 28, Update the
workflow’s checkout step to explicitly use the main ref and add a guard that
stops promotion unless the selected ref is main. Preserve fetch-depth: 0 and
ensure the subsequent tagging and push steps only run after this main-branch
validation.


- name: 다음 버전 태그 계산
id: version
run: |
git fetch --tags
LATEST=$(git tag --list 'v*' --sort=-v:refname | head -n1)
if [ -z "$LATEST" ]; then
LATEST="v0.0.0"
fi
echo "현재 최신 태그: $LATEST"

VERSION=${LATEST#v}
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
PATCH=$(echo "$VERSION" | cut -d. -f3)
Comment on lines +34 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- workflow context ---'
sed -n '1,90p' .github/workflows/promote-to-prod.yml

printf '%s\n' '--- arithmetic assignments and shell context ---'
rg -n -C 3 'MAJOR|MINOR|PATCH|VERSION|LATEST|shell:' .github/workflows/promote-to-prod.yml

printf '%s\n' '--- standalone Bash behavior probe ---'
bash -c '
set -u
probe() {
  local version=$1
  local major
  printf "VERSION=%q: " "$version"
  MAJOR=$(echo "$version" | cut -d. -f1)
  if MAJOR=$((MAJOR)); then
    printf "MAJOR=%s\n" "$MAJOR"
  else
    printf "arithmetic_status=%s\n" "$?"
  fi
}
probe "1.2.3"
probe "08.2.3"
probe "1+2.3.4"
probe "1\$(printf injected).2.3"
'

Repository: Runnect/Runnect-Spring-Boot-Server

Length of output: 3433


🏁 Script executed:

#!/bin/bash
set +e

printf '%s\n' '--- Bash version ---'
bash --version | head -n 1

printf '%s\n' '--- arithmetic-value probes ---'
for value in \
  '1+2' \
  '1+$(printf COMMAND_SUBSTITUTION >&2)' \
  '$(printf COMMAND_SUBSTITUTION >&2)' \
  '1#$(printf COMMAND_SUBSTITUTION >&2)' \
  '1;$(printf COMMAND_SUBSTITUTION >&2)' \
  '1));$(printf COMMAND_SUBSTITUTION >&2);((0' \
  '1[$(printf ARRAY_SUBSCRIPT >&2)]'
do
  printf 'value=%q -> ' "$value"
  VALUE=$value bash -c '
    set +e
    RESULT=$((VALUE + 1))
    STATUS=$?
    printf "result=%q status=%s\n" "$RESULT" "$STATUS"
  ' 2>&1
done

printf '%s\n' '--- candidate tag ref validation ---'
for ref in \
  'refs/tags/v1+2.3.4' \
  'refs/tags/v1.2.3-100' \
  'refs/tags/v1$(printf injected).2.3' \
  'refs/tags/v1.2.3/extra' \
  'refs/tags/v01.2.3'
do
  git check-ref-format "$ref" >/dev/null 2>&1
  printf '%s: status=%s\n' "$ref" "$?"
done

Repository: Runnect/Runnect-Spring-Boot-Server

Length of output: 1734


Reject non-semver tags before Bash arithmetic.

git tag --list 'v*' accepts valid Git tags that are not stable semver. Values such as 1+2 are evaluated as arithmetic expressions, which can generate an incorrect tag or fail the release step. Validate LATEST against strict stable semver before parsing its components.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/promote-to-prod.yml around lines 34 - 43, Validate LATEST
against strict stable semver immediately after selecting the latest tag and
before assigning VERSION or parsing MAJOR, MINOR, and PATCH. Reject non-matching
tags, including values with build metadata or other arithmetic-sensitive
characters, while preserving the v0.0.0 fallback for no matching tag.

Source: Linters/SAST tools


case "${{ inputs.bump }}" in
major)
MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0
;;
minor)
MINOR=$((MINOR + 1)); PATCH=0
;;
patch)
PATCH=$((PATCH + 1))
;;
esac

NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}"
echo "새 태그: $NEW_TAG"
echo "new_tag=$NEW_TAG" >> "$GITHUB_OUTPUT"

- name: 태그 생성 및 push (→ prod-cd.yml 트리거)
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "${{ steps.version.outputs.new_tag }}"
git push origin "${{ steps.version.outputs.new_tag }}"
Comment on lines +61 to +66

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- workflow files ---'
git ls-files '.github/workflows/*' 'README.md'

printf '%s\n' '--- promote-to-prod.yml ---'
cat -n .github/workflows/promote-to-prod.yml

printf '%s\n' '--- prod-cd.yml ---'
cat -n .github/workflows/prod-cd.yml

printf '%s\n' '--- references to promote/prod-cd/tag push/token ---'
rg -n -i 'promote-to-prod|prod-cd|GITHUB_TOKEN|persist-credentials|tag push|tag 생성|workflow_dispatch|on:' .github README.md

Repository: Runnect/Runnect-Spring-Boot-Server

Length of output: 8403


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- workflow files ---'
git ls-files '.github/workflows/*' 'README.md'

printf '%s\n' '--- promote-to-prod.yml ---'
cat -n .github/workflows/promote-to-prod.yml

printf '%s\n' '--- prod-cd.yml ---'
cat -n .github/workflows/prod-cd.yml

printf '%s\n' '--- related references ---'
rg -n -i 'promote-to-prod|prod-cd|GITHUB_TOKEN|persist-credentials|workflow_dispatch|tag 생성|tag push' .github README.md

Repository: Runnect/Runnect-Spring-Boot-Server

Length of output: 7506


🌐 Web query:

GitHub Actions documentation events triggered by GITHUB_TOKEN do not create new workflow runs actions/checkout persist-credentials default

💡 Result:

GitHub Actions is designed to prevent recursive workflow loops when using the default GITHUB_TOKEN. In general, events triggered by the GITHUB_TOKEN do not create new workflow runs [1][2][3]. There are a few key exceptions to this behavior: 1. Workflow Dispatch and Repository Dispatch: These events always trigger new workflow runs, even when initiated by the GITHUB_TOKEN [1][4]. 2. Pull Request Events (Opened, Synchronize, Reopened): When a workflow uses the GITHUB_TOKEN to create or update a pull request, the resulting event triggers workflow runs in an approval-required state [1][5]. These runs will not execute until a user with write access to the repository manually approves them [3][6]. Regarding the actions/checkout action, the persist-credentials input, which determines whether to configure the authentication token or SSH key in the local git configuration, has a default value of true [7][8][9]. While there have been community discussions and pull requests proposing to change this default to false to improve security, the official default remains true [7][10][11]. When set to true, the token is persisted in the local git config to enable authenticated git commands for that job, and it is removed during post-job cleanup [7][9].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

promote = Path(".github/workflows/promote-to-prod.yml").read_text()
prod_cd = Path(".github/workflows/prod-cd.yml").read_text()

checks = {
    "promote workflow_dispatch": bool(re.search(r"(?m)^\s+workflow_dispatch:\s*$", promote)),
    "contents write": bool(re.search(r"(?m)^\s+contents:\s*write\s*$", promote)),
    "checkout persist-credentials false": bool(
        re.search(r"(?m)^\s+persist-credentials:\s*false\s*$", promote)
    ),
    "checkout token override": bool(
        re.search(r"(?m)^\s+token:\s*(?!\$\{\{\s*github\.token\s*\}\})\S+", promote)
    ),
    "git tag command": bool(re.search(r"(?m)^\s*git tag ", promote)),
    "git push command": bool(re.search(r"(?m)^\s*git push ", promote)),
    "prod push tag trigger": bool(re.search(r"(?m)^\s+push:\s*$", prod_cd)),
    "prod v-star tag filter": bool(re.search(r"(?m)^\s+tags:\s*\[\s*['\"]v\*['\"]\s*\]", prod_cd)),
    "prod workflow_dispatch": bool(re.search(r"(?m)^\s+workflow_dispatch:\s*$", prod_cd)),
    "prod repository_dispatch": bool(re.search(r"(?m)^\s+repository_dispatch:\s*$", prod_cd)),
}

for name, result in checks.items():
    print(f"{name}: {result}")
PY

Repository: Runnect/Runnect-Spring-Boot-Server

Length of output: 439


Authenticate the tag push with a non-GITHUB_TOKEN credential.

actions/checkout@v3 persists GITHUB_TOKEN by default. GitHub does not trigger push workflows from events created by GITHUB_TOKEN. Since prod-cd.yml listens only for push tags matching v*, the tag is created but production deployment does not start. Set persist-credentials: false and authenticate the push with a PAT or GitHub App token, or invoke an explicit dispatch contract.

🧰 Tools
🪛 zizmor (1.29.0)

[info] 65-65: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 66-66: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/promote-to-prod.yml around lines 61 - 66, Update the
checkout configuration used by the promote workflow to set persist-credentials
to false, then authenticate the git push in the tag-generation step with a PAT
or GitHub App token rather than GITHUB_TOKEN. Ensure the tag push still targets
the version from steps.version.outputs.new_tag so the prod-cd.yml tag trigger
runs.

Source: Linters/SAST tools


- name: 요약
run: echo "## ${{ steps.version.outputs.new_tag }} 태그 push 완료 — RUNNECT-PROD-CD가 곧 트리거됩니다." >> "$GITHUB_STEP_SUMMARY"
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,14 @@
| staging (Render) | `main` push | 자동 배포 |
| 상용 (AWS CodeDeploy) | `v*` 형식의 git tag push | 수동 승격 |

merge → staging에서 자동 확인 → 문제 없으면 `git tag v1.x.x && git push origin v1.x.x`로 상용 배포. 버전은 느슨한 semver(기능 추가 minor, 버그 수정 patch)를 따른다.
merge → staging에서 자동 확인 → 문제 없으면 상용 배포. 태그를 로컬에서 직접 만들 필요 없이, **GitHub Actions의 `Promote to Production` workflow**(`workflow_dispatch`)를 실행하면 다음 semver 태그를 자동 계산해 push까지 해준다.

```bash
# Actions 탭에서 "Promote to Production" → Run workflow 버튼으로도 가능
gh workflow run promote-to-prod.yml -f bump=patch # patch | minor | major
```

이 태그 push가 `prod-cd.yml`의 트리거를 실행시켜 상용 배포로 이어진다.

### 🚩 Feature Flag

Expand Down
Loading