diff --git a/README.md b/README.md index 318d3cd..e89b935 100644 --- a/README.md +++ b/README.md @@ -111,9 +111,9 @@ jobs: command: "compose up --project-name my-project" ``` -### Disabling Output Capture +### Capturing Output -By default, the action captures the command's stdout as an output (`outputs.stdout`). For large deployments, this can exceed GitHub Actions' memory limits and cause the workflow to fail even though the deployment succeeded. If you don't need the stdout output, disable it: +Set `capture-output: true` to capture the command's stdout as an output (`outputs.stdout`). This is off by default because large deployments can exceed GitHub Actions' memory limits and cause the workflow to fail even though the deployment succeeded: ```yaml jobs: @@ -124,9 +124,41 @@ jobs: - name: Deploy uses: DefangLabs/defang-github-action@v2 with: - capture-output: false + capture-output: true ``` +### Publishing the Deployment Summary + +When the command is `compose up` (the default), the action surfaces the deployment result on the GitHub run instead of leaving it buried in the action log. After the command runs, the action calls `defang services --json` and: + +- writes a table of deployed services (public endpoints as links, internal services as code) to the [job summary](https://github.blog/news-insights/product-news/supercharging-github-actions-with-job-summaries/), and +- exposes the primary public `https://` endpoint as the `endpoint` output. + +Wire that output into the job's `environment.url` to get a clickable **"View deployment"** link on the run, the Environments tab, and the repo sidebar: + +```yaml +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: production + url: ${{ steps.deploy.outputs.endpoint }} + permissions: + contents: read + id-token: write + steps: + # [...] + - name: Deploy + id: deploy + uses: DefangLabs/defang-github-action@v2 + with: + command: "compose up" +``` + +Unlike `capture-output`, this writes straight to the job summary (1 MiB per step) rather than through the size-limited step-output channel, and `defang services` output is small and bounded — so it is safe even for large deployments where `capture-output` is disabled. + +Set `summary: "false"` to skip the summary, or `summary: "true"` to force it for commands other than `compose up`. + ### Full Example Here is a full example of a GitHub workflow that does everything we've discussed so far: @@ -142,6 +174,9 @@ on: jobs: test: runs-on: ubuntu-latest + environment: + name: production + url: ${{ steps.deploy.outputs.endpoint }} permissions: contents: read id-token: write @@ -151,6 +186,7 @@ jobs: uses: actions/checkout@v4 - name: Deploy + id: deploy uses: DefangLabs/defang-github-action@v2 with: cli-version: v3.5.2 diff --git a/action.yaml b/action.yaml index 3473a37..0232c5b 100644 --- a/action.yaml +++ b/action.yaml @@ -59,14 +59,21 @@ inputs: required: false default: "false" capture-output: - description: "Capture the command's stdout as an output. Disable for large deployments to avoid GitHub Actions memory limits." + description: "Capture the command's stdout as the 'stdout' output. Off by default: large deployments can exceed GitHub Actions memory limits." required: false - default: "true" + default: "false" + summary: + description: "After the command runs, write a table of deployed services to the GitHub job summary and expose the primary public endpoint as the 'endpoint' output. Uses 'defang services --json'. Defaults to enabled when 'command' is the default 'compose up'. Set to 'false' to disable, or 'true' to force-enable for other commands." + required: false + default: "" outputs: stdout: description: "The stdout of the command. Only available when capture-output is 'true'." value: ${{ steps.forward-output.outputs.stdout }} + endpoint: + description: "The primary public https endpoint of the deployed project (best-effort). Only set when the deployment summary runs (see 'summary')." + value: ${{ steps.summary.outputs.endpoint }} runs: using: "composite" @@ -111,6 +118,18 @@ runs: [ -n "${{ inputs['provider'] }}" ] && echo "DEFANG_PROVIDER=${{ inputs['provider'] }}" >> $GITHUB_ENV || true [ -n "${{ inputs['stack'] }}" ] && echo "DEFANG_STACK=${{ inputs['stack'] }}" >> $GITHUB_ENV || true [ -n "${{ inputs['project'] }}" ] && echo "COMPOSE_PROJECT_NAME=${{ inputs['project'] }}" >> $GITHUB_ENV || true + # Docker Compose (and the Defang CLI) natively read the compose file + # list from COMPOSE_FILE, so export that once instead of building a + # repeated -f/-f/-f list in every step. The unquoted echo collapses + # any whitespace (spaces or newlines) between filenames before tr + # joins them with ':'; COMPOSE_PATH_SEPARATOR pins the separator to + # ':' regardless of the runner OS. + if [ -n "$COMPOSE_FILES" ]; then + echo "COMPOSE_FILE=$(echo $COMPOSE_FILES | tr ' ' ':')" >> $GITHUB_ENV + echo "COMPOSE_PATH_SEPARATOR=:" >> $GITHUB_ENV + fi + env: + COMPOSE_FILES: ${{ inputs['compose-files'] }} - name: Login to Defang shell: bash @@ -123,13 +142,6 @@ runs: shell: bash if: ${{ inputs['config-env-vars'] != '' || inputs['config-vars-init-random'] != '' || inputs['config-env-vars-init'] != '' }} run: | - # Build compose file parameters - params=() - for filename in $COMPOSE_FILES; do - params+=("-f") - params+=("$filename") - done - # Check for empty environment variables (only for config vars from env) empty=() for source in $CONFIG_ENV_VARS $CONFIG_ENV_VARS_INIT; do @@ -152,26 +164,25 @@ runs: # Set random config variables if [ -n "$CONFIG_VARS_INIT_RANDOM" ]; then echo "Initialize randomly generated configs..." - echo defang config "${params[@]}" set --if-not-set --random $CONFIG_VARS_INIT_RANDOM - defang config "${params[@]}" set --if-not-set --random $CONFIG_VARS_INIT_RANDOM + echo defang config set --if-not-set --random $CONFIG_VARS_INIT_RANDOM + defang config set --if-not-set --random $CONFIG_VARS_INIT_RANDOM fi # Set init config variables if [ -n "$CONFIG_ENV_VARS_INIT" ]; then echo "Initialize configs..." - echo defang config "${params[@]}" set --if-not-set -e $CONFIG_ENV_VARS_INIT - defang config "${params[@]}" set --if-not-set -e $CONFIG_ENV_VARS_INIT + echo defang config set --if-not-set -e $CONFIG_ENV_VARS_INIT + defang config set --if-not-set -e $CONFIG_ENV_VARS_INIT fi # Set regular config variables if [ -n "$CONFIG_ENV_VARS" ]; then echo "Updating configs..." - echo defang config "${params[@]}" set -e $CONFIG_ENV_VARS - defang config "${params[@]}" set -e $CONFIG_ENV_VARS + echo defang config set -e $CONFIG_ENV_VARS + defang config set -e $CONFIG_ENV_VARS fi working-directory: ${{ inputs.cwd }} env: - COMPOSE_FILES: ${{ inputs['compose-files'] }} CONFIG_ENV_VARS: ${{ inputs['config-env-vars'] }} CONFIG_ENV_VARS_INIT: ${{ inputs['config-env-vars-init'] }} CONFIG_VARS_INIT_RANDOM: ${{ inputs['config-vars-init-random'] }} @@ -183,11 +194,8 @@ runs: working-directory: ${{ inputs.cwd }} run: | set -o pipefail + params=() - for filename in $COMPOSE_FILES; do - params+=("-f") - params+=("$filename") - done if [[ -n "${{ inputs['mode'] }}" ]]; then params+=("--mode=${{ inputs['mode'] }}") fi @@ -214,7 +222,6 @@ runs: fi env: COMMAND: ${{ inputs['command'] }} - COMPOSE_FILES: ${{ inputs['compose-files'] }} CONFIG_ENV_VARS: ${{ inputs['config-env-vars'] }} CONFIG_ENV_VARS_INIT: ${{ inputs['config-env-vars-init'] }} CONFIG_VARS_INIT_RANDOM: ${{ inputs['config-vars-init-random'] }} @@ -231,3 +238,47 @@ runs: env: STDOUT: ${{ steps.command.outputs.stdout }} + - name: Deployment summary + id: summary + if: ${{ inputs['summary'] == 'true' || (inputs['summary'] == '' && (inputs['command'] == 'compose up' || startsWith(inputs['command'], 'compose up '))) }} + shell: bash + working-directory: ${{ inputs.cwd }} + run: | + set -o pipefail + + if ! command -v jq >/dev/null 2>&1; then + echo "::notice::jq not found; skipping deployment summary" + exit 0 + fi + + # Fetch the deployed services as JSON. Compose files, stack, provider, + # and project come from the environment set earlier, so they don't + # need to be passed again. + # Only fall back to an empty array on failure, so a successful run + # never yields two concatenated JSON values. + if services=$(defang services --json 2>/dev/null); then + : + else + services='[]' + fi + + # Expose the primary public endpoint (first https:// service) as an output. + endpoint=$(echo "$services" | jq -r 'map(select((.Endpoint // "") | startswith("https://"))) | .[0].Endpoint // empty') + echo "endpoint=$endpoint" >> "$GITHUB_OUTPUT" + + # Render a services table to the job summary. Public endpoints become + # links; internal services are shown as code. Deployments with no + # services, or with services but no public endpoint, report + # "No services found." per the summary's contract. + { + echo "## 🚀 Deployed services" + echo "" + if [ -n "$endpoint" ] && [ "$(echo "$services" | jq 'length')" -gt 0 ]; then + echo "| Service | Status | Endpoint |" + echo "|---|---|---|" + echo "$services" | jq -r '.[] | "| \(.Service) | \(.Status) | " + (if ((.Endpoint // "") | startswith("https://")) then "[\(.Endpoint)](\(.Endpoint))" else "`\(.Endpoint)`" end) + " |"' + else + echo "_No services found._" + fi + } >> "$GITHUB_STEP_SUMMARY" +