diff --git a/.github/workflows/declarative-release-action.yml b/.github/workflows/declarative-release-action.yml new file mode 100644 index 0000000..fce55ab --- /dev/null +++ b/.github/workflows/declarative-release-action.yml @@ -0,0 +1,93 @@ +name: Declarative rollout using bytebase-action image + +on: + push: + branches: + - main + paths: + - "schema/*.sql" + +# cancel previous workflow run if a new workflow run is triggered +# to prevent multiple rollout +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +env: + BYTEBASE_URL: https://demo.bytebase.com + BYTEBASE_SERVICE_ACCOUNT: api@service.bytebase.com + BYTEBASE_SERVICE_ACCOUNT_SECRET: ${{ secrets.BYTEBASE_SERVICE_ACCOUNT_SECRET }} + BYTEBASE_PROJECT: "projects/hr" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build app and upload + run: | + echo "Building..." + echo "Build done!" + echo "Uploading..." + echo "Upload done!" + create-rollout: + needs: build + runs-on: ubuntu-latest # use self-hosted machines if your Bytebase runs in internal networks. + container: + image: bytebase/bytebase-action:latest + outputs: + bytebase-plan: ${{ steps.set-output.outputs.plan }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Roll out database change + env: + BYTEBASE_TARGETS: "instances/test-sample-instance/databases/hr_test,instances/prod-sample-instance/databases/hr_prod" + FILE_PATTERN: "schema/*.sql" + BYTEBASE_OUTPUT: ${{ runner.temp }}/bytebase-metadata.json + run: | + bytebase-action rollout --url=${{ env.BYTEBASE_URL }} --service-account=${{ env.BYTEBASE_SERVICE_ACCOUNT }} --service-account-secret=${{ env.BYTEBASE_SERVICE_ACCOUNT_SECRET }} --project=${{ env.BYTEBASE_PROJECT }} --file-pattern=${{ env.FILE_PATTERN }} --targets=${{ env.BYTEBASE_TARGETS }} --declarative --output=${{ env.BYTEBASE_OUTPUT }} + - name: Set output + id: set-output + run: | + PLAN=$(jq -r .plan ${{ runner.temp }}/bytebase-metadata.json) + echo "plan=$PLAN" >> $GITHUB_OUTPUT + deploy-to-test: + needs: create-rollout + runs-on: ubuntu-latest # use self-hosted machines if your Bytebase runs in internal networks. + environment: test + container: + image: bytebase/bytebase-action:latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Roll out database change + env: + BYTEBASE_TARGET_STAGE: environments/test + run: | + bytebase-action rollout --url=${{ env.BYTEBASE_URL }} --service-account=${{ env.BYTEBASE_SERVICE_ACCOUNT }} --service-account-secret=${{ env.BYTEBASE_SERVICE_ACCOUNT_SECRET }} --project=${{ env.BYTEBASE_PROJECT }} --target-stage=${{ env.BYTEBASE_TARGET_STAGE }} --plan=${{ needs.create-rollout.outputs.bytebase-plan }} + - name: Deploy app + run: | + echo "Deploying app to test environment..." + echo "Deploy app to test environment done!" + deploy-to-prod: + needs: + - deploy-to-test + - create-rollout + runs-on: ubuntu-latest + environment: prod + container: + image: bytebase/bytebase-action:latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: rollout + env: + BYTEBASE_TARGET_STAGE: environments/prod + run: | + bytebase-action rollout --url=${{ env.BYTEBASE_URL }} --service-account=${{ env.BYTEBASE_SERVICE_ACCOUNT }} --service-account-secret=${{ env.BYTEBASE_SERVICE_ACCOUNT_SECRET }} --project=${{ env.BYTEBASE_PROJECT }} --target-stage=${{ env.BYTEBASE_TARGET_STAGE }} --plan=${{ needs.create-rollout.outputs.bytebase-plan }} + - name: Deploy app + run: | + echo "Deploying app to prod environment..." + echo "Deploy app to prod environment done!" diff --git a/schema/schema.sql b/schema/schema.sql new file mode 100644 index 0000000..98d0a02 --- /dev/null +++ b/schema/schema.sql @@ -0,0 +1,120 @@ +CREATE TABLE public.employee ( + emp_no SERIAL NOT NULL, + birth_date DATE NOT NULL, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + gender TEXT NOT NULL CHECK (gender IN('M', 'F')) NOT NULL, + hire_date DATE NOT NULL, + PRIMARY KEY (emp_no) +); + +CREATE INDEX idx_employee_hire_date ON public.employee (hire_date); + +CREATE TABLE public.department ( + dept_no TEXT NOT NULL, + dept_name TEXT NOT NULL, + PRIMARY KEY (dept_no), + UNIQUE (dept_name) +); + +CREATE TABLE public.dept_manager ( + emp_no INT NOT NULL, + dept_no TEXT NOT NULL, + from_date DATE NOT NULL, + to_date DATE NOT NULL, + FOREIGN KEY (emp_no) REFERENCES employee (emp_no) ON DELETE CASCADE, + FOREIGN KEY (dept_no) REFERENCES department (dept_no) ON DELETE CASCADE, + PRIMARY KEY (emp_no, dept_no) +); + +CREATE TABLE public.dept_emp ( + emp_no INT NOT NULL, + dept_no TEXT NOT NULL, + from_date DATE NOT NULL, + to_date DATE NOT NULL, + FOREIGN KEY (emp_no) REFERENCES employee (emp_no) ON DELETE CASCADE, + FOREIGN KEY (dept_no) REFERENCES department (dept_no) ON DELETE CASCADE, + PRIMARY KEY (emp_no, dept_no) +); + +CREATE TABLE public.title ( + emp_no INT NOT NULL, + title TEXT NOT NULL, + from_date DATE NOT NULL, + to_date DATE, + FOREIGN KEY (emp_no) REFERENCES employee (emp_no) ON DELETE CASCADE, + PRIMARY KEY (emp_no, title, from_date) +); + +CREATE TABLE public.salary ( + emp_no INT NOT NULL, + amount INT NOT NULL, + from_date DATE NOT NULL, + to_date DATE NOT NULL, + FOREIGN KEY (emp_no) REFERENCES employee (emp_no) ON DELETE CASCADE, + PRIMARY KEY (emp_no, from_date) +); + +CREATE INDEX idx_salary_amount ON public.salary (amount); + +CREATE TABLE public.audit ( + id SERIAL PRIMARY KEY, + operation TEXT NOT NULL, + query TEXT, + user_name TEXT NOT NULL, + changed_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_audit_operation ON public.audit (operation); +CREATE INDEX idx_audit_username ON public.audit (user_name); +CREATE INDEX idx_audit_changed_at ON public.audit (changed_at); + +CREATE OR REPLACE FUNCTION public.log_dml_operations() RETURNS TRIGGER AS $$ +BEGIN + IF (TG_OP = 'INSERT') THEN + INSERT INTO public.audit (operation, query, user_name) + VALUES ('INSERT', current_query(), current_user); + RETURN NEW; + ELSIF (TG_OP = 'UPDATE') THEN + INSERT INTO public.audit (operation, query, user_name) + VALUES ('UPDATE', current_query(), current_user); + RETURN NEW; + ELSIF (TG_OP = 'DELETE') THEN + INSERT INTO public.audit (operation, query, user_name) + VALUES ('DELETE', current_query(), current_user); + RETURN OLD; + END IF; + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +-- only log update and delete, otherwise, it will cause too much change. +CREATE TRIGGER salary_log_trigger +AFTER UPDATE OR DELETE ON public.salary +FOR EACH ROW +EXECUTE FUNCTION public.log_dml_operations(); + +CREATE OR REPLACE VIEW public.dept_emp_latest_date AS +SELECT + emp_no, + MAX( + from_date) AS from_date, + MAX( + to_date) AS to_date +FROM + public.dept_emp +GROUP BY + emp_no; + +-- shows only the current department for each employee +CREATE OR REPLACE VIEW public.current_dept_emp AS +SELECT + l.emp_no, + dept_no, + l.from_date, + l.to_date +FROM + public.dept_emp d + INNER JOIN public.dept_emp_latest_date l ON d.emp_no = l.emp_no + AND d.from_date = l.from_date + AND l.to_date = d.to_date; diff --git a/schema/users.sql b/schema/users.sql new file mode 100644 index 0000000..b66be12 --- /dev/null +++ b/schema/users.sql @@ -0,0 +1,5 @@ +CREATE TABLE public.users ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE +);