diff --git a/.github/workflows/resolve-env.yml b/.github/workflows/resolve-env.yml index 7d20fc3..49af8e6 100644 --- a/.github/workflows/resolve-env.yml +++ b/.github/workflows/resolve-env.yml @@ -1,8 +1,9 @@ name: Resolve env # Resolves the deploy environment (dev/staging/prod) for the current branch -# via the branch-env-map action, exposing it as a job output so callers can -# reference it in a downstream job's `name:` (e.g. `Deploy [${{ needs.resolve-env.outputs.env }}]`). +# using the same mapping as the branch-env-map action, exposing it as a job +# output so callers can reference it in a downstream job's `name:` (e.g. +# `Deploy [${{ needs.resolve-env.outputs.env }}]`). # Job names may use the `needs` context, but not composite action outputs # directly, so this indirection is what makes that possible. @@ -34,10 +35,30 @@ jobs: outputs: env: ${{ steps.map.outputs.env }} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false + # Same mapping as actions/branch-env-map (a test keeps them identical). + # Inlined because a reusable workflow can't reach `./actions/...` in its + # own repo: checkout gives it the caller's tree, not voidflow's. - id: map - uses: ./actions/branch-env-map - with: - single-environment: ${{ inputs.single-environment }} + shell: bash + env: + REF_NAME: ${{ github.ref_name }} + EVENT_NAME: ${{ github.event_name }} + SINGLE: ${{ inputs.single-environment }} + run: | + if [[ "$SINGLE" == "true" ]]; then + if [[ "$EVENT_NAME" == pull_request* ]]; then + echo "env=preview" >> "$GITHUB_OUTPUT" + elif [[ "$REF_NAME" == main ]]; then + echo "env=prod" >> "$GITHUB_OUTPUT" + else + echo "::error::Refusing to deploy unknown ref: $REF_NAME (expected main or a pull request)" >&2 + exit 1 + fi + exit 0 + fi + case "$REF_NAME" in + main) echo "env=dev" >> "$GITHUB_OUTPUT" ;; + next) echo "env=staging" >> "$GITHUB_OUTPUT" ;; + live) echo "env=prod" >> "$GITHUB_OUTPUT" ;; + *) echo "::error::Refusing to deploy unknown branch: $REF_NAME (expected main, next, or live)" >&2; exit 1 ;; + esac diff --git a/test/branch-env-map.test.ts b/test/branch-env-map.test.ts new file mode 100644 index 0000000..6f0fa7a --- /dev/null +++ b/test/branch-env-map.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test } from 'bun:test' +import { mkdtempSync, readFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +const read = (p: string) => readFileSync(new URL(`../${p}`, import.meta.url), 'utf8') + +const actionScript: string = Bun.YAML.parse(read('actions/branch-env-map/action.yml')).runs.steps.find( + (s: any) => s.id === 'map', +).run +const workflowScript: string = Bun.YAML.parse(read('.github/workflows/resolve-env.yml')).jobs[ + 'resolve-env' +].steps.find((s: any) => s.id === 'map').run + +async function map(script: string, ref: string, event: string, single: boolean) { + const out = join(mkdtempSync(join(tmpdir(), 'voidflow-env-')), 'out') + await Bun.write(out, '') + const proc = Bun.spawn(['bash', '-c', script], { + env: { + PATH: process.env.PATH, + REF_NAME: ref, + EVENT_NAME: event, + SINGLE: String(single), + GITHUB_OUTPUT: out, + }, + stdout: 'pipe', + stderr: 'pipe', + }) + const code = await proc.exited + return { code, env: readFileSync(out, 'utf8').trim() } +} + +describe('resolve-env.yml carries the same mapping as branch-env-map', () => { + test('scripts are identical (no drift)', () => { + expect(workflowScript).toBe(actionScript) + }) + + test('resolve-env.yml has no `uses` steps (a local ./actions path or checkout cannot work cross-repo)', () => { + const steps = Bun.YAML.parse(read('.github/workflows/resolve-env.yml')).jobs['resolve-env'].steps + expect(steps.filter((s: any) => s.uses)).toEqual([]) + }) +}) + +describe.each([ + ['action', actionScript], + ['workflow', workflowScript], +])('%s script', (_name, script) => { + test.each([ + ['main', 'push', 'env=dev'], + ['next', 'push', 'env=staging'], + ['live', 'push', 'env=prod'], + ])('default mode: %s on %s', async (ref, event, expected) => { + expect(await map(script, ref, event, false)).toEqual({ code: 0, env: expected }) + }) + + test('default mode rejects unknown branches', async () => { + expect((await map(script, 'feature/x', 'push', false)).code).toBe(1) + }) + + test.each([ + ['main', 'push', 'env=prod'], + ['123/merge', 'pull_request', 'env=preview'], + ['feature/x', 'pull_request', 'env=preview'], + ])('single-environment: %s on %s', async (ref, event, expected) => { + expect(await map(script, ref, event, true)).toEqual({ code: 0, env: expected }) + }) + + test('single-environment rejects non-main pushes', async () => { + expect((await map(script, 'feature/x', 'push', true)).code).toBe(1) + }) +})